From 4f49b16079b27d7ce633619e4cddfdf2bffbc952 Mon Sep 17 00:00:00 2001 From: cashmere Date: Fri, 4 Sep 2026 09:05:24 +0200 Subject: initial commit --- autosetup/default.auto | 25 + compat/zlib/contrib/delphi/ZLib.pas | 557 ++ compat/zlib/contrib/delphi/ZLibConst.pas | 11 + compat/zlib/contrib/delphi/readme.txt | 76 + compat/zlib/contrib/delphi/zlibd32.mak | 99 + compat/zlib/contrib/dotzlib/DotZLib.build | 33 + compat/zlib/contrib/dotzlib/DotZLib.chm | Bin 0 -> 72726 bytes compat/zlib/contrib/dotzlib/DotZLib.sln | 21 + .../zlib/contrib/dotzlib/DotZLib/AssemblyInfo.cs | 58 + .../zlib/contrib/dotzlib/DotZLib/ChecksumImpl.cs | 202 + .../zlib/contrib/dotzlib/DotZLib/CircularBuffer.cs | 83 + compat/zlib/contrib/dotzlib/DotZLib/CodecBase.cs | 198 + compat/zlib/contrib/dotzlib/DotZLib/Deflater.cs | 106 + compat/zlib/contrib/dotzlib/DotZLib/DotZLib.cs | 288 + compat/zlib/contrib/dotzlib/DotZLib/DotZLib.csproj | 141 + compat/zlib/contrib/dotzlib/DotZLib/GZipStream.cs | 301 + compat/zlib/contrib/dotzlib/DotZLib/Inflater.cs | 105 + compat/zlib/contrib/dotzlib/DotZLib/UnitTests.cs | 274 + compat/zlib/contrib/dotzlib/LICENSE_1_0.txt | 23 + compat/zlib/contrib/dotzlib/readme.txt | 58 + compat/zlib/deflate.c | 2185 ++++++++ compat/zlib/deflate.h | 383 ++ npins/default.nix | 260 + skins/ardoise/details.txt | 5 + skins/ardoise/images/delindent.svg | 1 + skins/ardoise/images/delindent_h.svg | 1 + skins/ardoise/images/dir.svg | 1 + skins/black_and_white/details.txt | 4 + skins/blitz/details.txt | 4 + skins/blitz/dir.png | Bin 0 -> 180 bytes skins/copland/details.txt | 9 + skins/darkmode/css.txt | 603 ++ skins/darkmode/details.txt | 4 + skins/darkmode/footer.txt | 8 + skins/darkmode/header.txt | 31 + skins/default/README.md | 10 + skins/default/css.txt | 789 +++ skins/default/details.txt | 6 + skins/default/footer.txt | 5 + skins/default/header.txt | 67 + skins/eagle/details.txt | 5 + skins/etienne/details.txt | 4 + skins/khaki/details.txt | 4 + skins/original/details.txt | 4 + skins/plain_gray/details.txt | 4 + skins/xekri/details.txt | 4 + src/db.c | 5733 ++++++++++++++++++++ src/default.css | 2194 ++++++++ src/delta.c | 729 +++ src/deltacmd.c | 198 + src/deltafunc.c | 530 ++ src/descendants.c | 725 +++ src/diff.c | 4054 ++++++++++++++ src/diff.js | 73 + src/diff.tcl | 707 +++ src/diffcmd.c | 1595 ++++++ src/dispatch.c | 1971 +++++++ src/doc.c | 1277 +++++ src/sounds/d.wav | Bin 0 -> 6044 bytes test/delta1.test | 60 + test/diff-test-1.wiki | 60 + test/diff.test | 162 + tools/copland/dist/copland-skin.config | 827 +++ tools/cvs2fossil/doc/LICENSE | 19 + tools/cvs2fossil/doc/README | 7 + tools/cvs2fossil/lib/dot.tcl | 108 + tools/decode-email.c | 163 + win/include/dirent.h | 1160 ++++ www/css/diff.md | 112 + www/defcsp.md | 490 ++ www/delta-manifests.md | 246 + www/delta_encoder_algorithm.wiki | 248 + www/delta_format.wiki | 316 ++ www/server/debian/index.md | 6 + www/server/debian/nginx.md | 395 ++ www/server/debian/service.md | 259 + 76 files changed, 31484 insertions(+) create mode 100644 autosetup/default.auto create mode 100644 compat/zlib/contrib/delphi/ZLib.pas create mode 100644 compat/zlib/contrib/delphi/ZLibConst.pas create mode 100644 compat/zlib/contrib/delphi/readme.txt create mode 100644 compat/zlib/contrib/delphi/zlibd32.mak create mode 100644 compat/zlib/contrib/dotzlib/DotZLib.build create mode 100644 compat/zlib/contrib/dotzlib/DotZLib.chm create mode 100644 compat/zlib/contrib/dotzlib/DotZLib.sln create mode 100644 compat/zlib/contrib/dotzlib/DotZLib/AssemblyInfo.cs create mode 100644 compat/zlib/contrib/dotzlib/DotZLib/ChecksumImpl.cs create mode 100644 compat/zlib/contrib/dotzlib/DotZLib/CircularBuffer.cs create mode 100644 compat/zlib/contrib/dotzlib/DotZLib/CodecBase.cs create mode 100644 compat/zlib/contrib/dotzlib/DotZLib/Deflater.cs create mode 100644 compat/zlib/contrib/dotzlib/DotZLib/DotZLib.cs create mode 100644 compat/zlib/contrib/dotzlib/DotZLib/DotZLib.csproj create mode 100644 compat/zlib/contrib/dotzlib/DotZLib/GZipStream.cs create mode 100644 compat/zlib/contrib/dotzlib/DotZLib/Inflater.cs create mode 100644 compat/zlib/contrib/dotzlib/DotZLib/UnitTests.cs create mode 100644 compat/zlib/contrib/dotzlib/LICENSE_1_0.txt create mode 100644 compat/zlib/contrib/dotzlib/readme.txt create mode 100644 compat/zlib/deflate.c create mode 100644 compat/zlib/deflate.h create mode 100644 npins/default.nix create mode 100644 skins/ardoise/details.txt create mode 100644 skins/ardoise/images/delindent.svg create mode 100644 skins/ardoise/images/delindent_h.svg create mode 100644 skins/ardoise/images/dir.svg create mode 100644 skins/black_and_white/details.txt create mode 100644 skins/blitz/details.txt create mode 100644 skins/blitz/dir.png create mode 100644 skins/copland/details.txt create mode 100644 skins/darkmode/css.txt create mode 100644 skins/darkmode/details.txt create mode 100644 skins/darkmode/footer.txt create mode 100644 skins/darkmode/header.txt create mode 100644 skins/default/README.md create mode 100644 skins/default/css.txt create mode 100644 skins/default/details.txt create mode 100644 skins/default/footer.txt create mode 100644 skins/default/header.txt create mode 100644 skins/eagle/details.txt create mode 100644 skins/etienne/details.txt create mode 100644 skins/khaki/details.txt create mode 100644 skins/original/details.txt create mode 100644 skins/plain_gray/details.txt create mode 100644 skins/xekri/details.txt create mode 100644 src/db.c create mode 100644 src/default.css create mode 100644 src/delta.c create mode 100644 src/deltacmd.c create mode 100644 src/deltafunc.c create mode 100644 src/descendants.c create mode 100644 src/diff.c create mode 100644 src/diff.js create mode 100644 src/diff.tcl create mode 100644 src/diffcmd.c create mode 100644 src/dispatch.c create mode 100644 src/doc.c create mode 100644 src/sounds/d.wav create mode 100644 test/delta1.test create mode 100644 test/diff-test-1.wiki create mode 100644 test/diff.test create mode 100644 tools/copland/dist/copland-skin.config create mode 100644 tools/cvs2fossil/doc/LICENSE create mode 100644 tools/cvs2fossil/doc/README create mode 100644 tools/cvs2fossil/lib/dot.tcl create mode 100644 tools/decode-email.c create mode 100755 win/include/dirent.h create mode 100644 www/css/diff.md create mode 100644 www/defcsp.md create mode 100644 www/delta-manifests.md create mode 100644 www/delta_encoder_algorithm.wiki create mode 100644 www/delta_format.wiki create mode 100644 www/server/debian/index.md create mode 100644 www/server/debian/nginx.md create mode 100644 www/server/debian/service.md (limited to '[^,.:;@$]*/d*') diff --git a/autosetup/default.auto b/autosetup/default.auto new file mode 100644 index 0000000..b36e0f8 --- /dev/null +++ b/autosetup/default.auto @@ -0,0 +1,25 @@ +# Copyright (c) 2012 WorkWare Systems http://www.workware.net.au/ +# All rights reserved + +# Auto-load module for 'make' build system integration + +use init + +autosetup_add_init_type make {Simple "make" build system} { + autosetup_check_create auto.def \ +{# Initial auto.def created by 'autosetup --init=make' + +use cc + +# Add any user options here +options { +} + +make-config-header config.h +make-template Makefile.in +} + + if {![file exists Makefile.in]} { + puts "Note: I don't see Makefile.in. You will probably need to create one." + } +} diff --git a/compat/zlib/contrib/delphi/ZLib.pas b/compat/zlib/contrib/delphi/ZLib.pas new file mode 100644 index 0000000..cda3b09 --- /dev/null +++ b/compat/zlib/contrib/delphi/ZLib.pas @@ -0,0 +1,557 @@ +{*******************************************************} +{ } +{ Borland Delphi Supplemental Components } +{ ZLIB Data Compression Interface Unit } +{ } +{ Copyright (c) 1997,99 Borland Corporation } +{ } +{*******************************************************} + +{ Updated for zlib 1.2.x by Cosmin Truta } + +unit ZLib; + +interface + +uses SysUtils, Classes; + +type + TAlloc = function (AppData: Pointer; Items, Size: Integer): Pointer; cdecl; + TFree = procedure (AppData, Block: Pointer); cdecl; + + // Internal structure. Ignore. + TZStreamRec = packed record + next_in: PChar; // next input byte + avail_in: Integer; // number of bytes available at next_in + total_in: Longint; // total nb of input bytes read so far + + next_out: PChar; // next output byte should be put here + avail_out: Integer; // remaining free space at next_out + total_out: Longint; // total nb of bytes output so far + + msg: PChar; // last error message, NULL if no error + internal: Pointer; // not visible by applications + + zalloc: TAlloc; // used to allocate the internal state + zfree: TFree; // used to free the internal state + AppData: Pointer; // private data object passed to zalloc and zfree + + data_type: Integer; // best guess about the data type: ascii or binary + adler: Longint; // adler32 value of the uncompressed data + reserved: Longint; // reserved for future use + end; + + // Abstract ancestor class + TCustomZlibStream = class(TStream) + private + FStrm: TStream; + FStrmPos: Integer; + FOnProgress: TNotifyEvent; + FZRec: TZStreamRec; + FBuffer: array [Word] of Char; + protected + procedure Progress(Sender: TObject); dynamic; + property OnProgress: TNotifyEvent read FOnProgress write FOnProgress; + constructor Create(Strm: TStream); + end; + +{ TCompressionStream compresses data on the fly as data is written to it, and + stores the compressed data to another stream. + + TCompressionStream is write-only and strictly sequential. Reading from the + stream will raise an exception. Using Seek to move the stream pointer + will raise an exception. + + Output data is cached internally, written to the output stream only when + the internal output buffer is full. All pending output data is flushed + when the stream is destroyed. + + The Position property returns the number of uncompressed bytes of + data that have been written to the stream so far. + + CompressionRate returns the on-the-fly percentage by which the original + data has been compressed: (1 - (CompressedBytes / UncompressedBytes)) * 100 + If raw data size = 100 and compressed data size = 25, the CompressionRate + is 75% + + The OnProgress event is called each time the output buffer is filled and + written to the output stream. This is useful for updating a progress + indicator when you are writing a large chunk of data to the compression + stream in a single call.} + + + TCompressionLevel = (clNone, clFastest, clDefault, clMax); + + TCompressionStream = class(TCustomZlibStream) + private + function GetCompressionRate: Single; + public + constructor Create(CompressionLevel: TCompressionLevel; Dest: TStream); + destructor Destroy; override; + function Read(var Buffer; Count: Longint): Longint; override; + function Write(const Buffer; Count: Longint): Longint; override; + function Seek(Offset: Longint; Origin: Word): Longint; override; + property CompressionRate: Single read GetCompressionRate; + property OnProgress; + end; + +{ TDecompressionStream decompresses data on the fly as data is read from it. + + Compressed data comes from a separate source stream. TDecompressionStream + is read-only and unidirectional; you can seek forward in the stream, but not + backwards. The special case of setting the stream position to zero is + allowed. Seeking forward decompresses data until the requested position in + the uncompressed data has been reached. Seeking backwards, seeking relative + to the end of the stream, requesting the size of the stream, and writing to + the stream will raise an exception. + + The Position property returns the number of bytes of uncompressed data that + have been read from the stream so far. + + The OnProgress event is called each time the internal input buffer of + compressed data is exhausted and the next block is read from the input stream. + This is useful for updating a progress indicator when you are reading a + large chunk of data from the decompression stream in a single call.} + + TDecompressionStream = class(TCustomZlibStream) + public + constructor Create(Source: TStream); + destructor Destroy; override; + function Read(var Buffer; Count: Longint): Longint; override; + function Write(const Buffer; Count: Longint): Longint; override; + function Seek(Offset: Longint; Origin: Word): Longint; override; + property OnProgress; + end; + + + +{ CompressBuf compresses data, buffer to buffer, in one call. + In: InBuf = ptr to compressed data + InBytes = number of bytes in InBuf + Out: OutBuf = ptr to newly allocated buffer containing decompressed data + OutBytes = number of bytes in OutBuf } +procedure CompressBuf(const InBuf: Pointer; InBytes: Integer; + out OutBuf: Pointer; out OutBytes: Integer); + + +{ DecompressBuf decompresses data, buffer to buffer, in one call. + In: InBuf = ptr to compressed data + InBytes = number of bytes in InBuf + OutEstimate = zero, or est. size of the decompressed data + Out: OutBuf = ptr to newly allocated buffer containing decompressed data + OutBytes = number of bytes in OutBuf } +procedure DecompressBuf(const InBuf: Pointer; InBytes: Integer; + OutEstimate: Integer; out OutBuf: Pointer; out OutBytes: Integer); + +{ DecompressToUserBuf decompresses data, buffer to buffer, in one call. + In: InBuf = ptr to compressed data + InBytes = number of bytes in InBuf + Out: OutBuf = ptr to user-allocated buffer to contain decompressed data + BufSize = number of bytes in OutBuf } +procedure DecompressToUserBuf(const InBuf: Pointer; InBytes: Integer; + const OutBuf: Pointer; BufSize: Integer); + +const + zlib_version = '1.3.2'; + +type + EZlibError = class(Exception); + ECompressionError = class(EZlibError); + EDecompressionError = class(EZlibError); + +implementation + +uses ZLibConst; + +const + Z_NO_FLUSH = 0; + Z_PARTIAL_FLUSH = 1; + Z_SYNC_FLUSH = 2; + Z_FULL_FLUSH = 3; + Z_FINISH = 4; + + Z_OK = 0; + Z_STREAM_END = 1; + Z_NEED_DICT = 2; + Z_ERRNO = (-1); + Z_STREAM_ERROR = (-2); + Z_DATA_ERROR = (-3); + Z_MEM_ERROR = (-4); + Z_BUF_ERROR = (-5); + Z_VERSION_ERROR = (-6); + + Z_NO_COMPRESSION = 0; + Z_BEST_SPEED = 1; + Z_BEST_COMPRESSION = 9; + Z_DEFAULT_COMPRESSION = (-1); + + Z_FILTERED = 1; + Z_HUFFMAN_ONLY = 2; + Z_RLE = 3; + Z_DEFAULT_STRATEGY = 0; + + Z_BINARY = 0; + Z_ASCII = 1; + Z_UNKNOWN = 2; + + Z_DEFLATED = 8; + + +{$L adler32.obj} +{$L compress.obj} +{$L crc32.obj} +{$L deflate.obj} +{$L infback.obj} +{$L inffast.obj} +{$L inflate.obj} +{$L inftrees.obj} +{$L trees.obj} +{$L uncompr.obj} +{$L zutil.obj} + +procedure adler32; external; +procedure compressBound; external; +procedure crc32; external; +procedure deflateInit2_; external; +procedure deflateParams; external; + +function _malloc(Size: Integer): Pointer; cdecl; +begin + Result := AllocMem(Size); +end; + +procedure _free(Block: Pointer); cdecl; +begin + FreeMem(Block); +end; + +procedure _memset(P: Pointer; B: Byte; count: Integer); cdecl; +begin + FillChar(P^, count, B); +end; + +procedure _memcpy(dest, source: Pointer; count: Integer); cdecl; +begin + Move(source^, dest^, count); +end; + + + +// deflate compresses data +function deflateInit_(var strm: TZStreamRec; level: Integer; version: PChar; + recsize: Integer): Integer; external; +function deflate(var strm: TZStreamRec; flush: Integer): Integer; external; +function deflateEnd(var strm: TZStreamRec): Integer; external; + +// inflate decompresses data +function inflateInit_(var strm: TZStreamRec; version: PChar; + recsize: Integer): Integer; external; +function inflate(var strm: TZStreamRec; flush: Integer): Integer; external; +function inflateEnd(var strm: TZStreamRec): Integer; external; +function inflateReset(var strm: TZStreamRec): Integer; external; + + +function zlibAllocMem(AppData: Pointer; Items, Size: Integer): Pointer; cdecl; +begin +// GetMem(Result, Items*Size); + Result := AllocMem(Items * Size); +end; + +procedure zlibFreeMem(AppData, Block: Pointer); cdecl; +begin + FreeMem(Block); +end; + +{function zlibCheck(code: Integer): Integer; +begin + Result := code; + if code < 0 then + raise EZlibError.Create('error'); //!! +end;} + +function CCheck(code: Integer): Integer; +begin + Result := code; + if code < 0 then + raise ECompressionError.Create('error'); //!! +end; + +function DCheck(code: Integer): Integer; +begin + Result := code; + if code < 0 then + raise EDecompressionError.Create('error'); //!! +end; + +procedure CompressBuf(const InBuf: Pointer; InBytes: Integer; + out OutBuf: Pointer; out OutBytes: Integer); +var + strm: TZStreamRec; + P: Pointer; +begin + FillChar(strm, sizeof(strm), 0); + strm.zalloc := zlibAllocMem; + strm.zfree := zlibFreeMem; + OutBytes := ((InBytes + (InBytes div 10) + 12) + 255) and not 255; + GetMem(OutBuf, OutBytes); + try + strm.next_in := InBuf; + strm.avail_in := InBytes; + strm.next_out := OutBuf; + strm.avail_out := OutBytes; + CCheck(deflateInit_(strm, Z_BEST_COMPRESSION, zlib_version, sizeof(strm))); + try + while CCheck(deflate(strm, Z_FINISH)) <> Z_STREAM_END do + begin + P := OutBuf; + Inc(OutBytes, 256); + ReallocMem(OutBuf, OutBytes); + strm.next_out := PChar(Integer(OutBuf) + (Integer(strm.next_out) - Integer(P))); + strm.avail_out := 256; + end; + finally + CCheck(deflateEnd(strm)); + end; + ReallocMem(OutBuf, strm.total_out); + OutBytes := strm.total_out; + except + FreeMem(OutBuf); + raise + end; +end; + + +procedure DecompressBuf(const InBuf: Pointer; InBytes: Integer; + OutEstimate: Integer; out OutBuf: Pointer; out OutBytes: Integer); +var + strm: TZStreamRec; + P: Pointer; + BufInc: Integer; +begin + FillChar(strm, sizeof(strm), 0); + strm.zalloc := zlibAllocMem; + strm.zfree := zlibFreeMem; + BufInc := (InBytes + 255) and not 255; + if OutEstimate = 0 then + OutBytes := BufInc + else + OutBytes := OutEstimate; + GetMem(OutBuf, OutBytes); + try + strm.next_in := InBuf; + strm.avail_in := InBytes; + strm.next_out := OutBuf; + strm.avail_out := OutBytes; + DCheck(inflateInit_(strm, zlib_version, sizeof(strm))); + try + while DCheck(inflate(strm, Z_NO_FLUSH)) <> Z_STREAM_END do + begin + P := OutBuf; + Inc(OutBytes, BufInc); + ReallocMem(OutBuf, OutBytes); + strm.next_out := PChar(Integer(OutBuf) + (Integer(strm.next_out) - Integer(P))); + strm.avail_out := BufInc; + end; + finally + DCheck(inflateEnd(strm)); + end; + ReallocMem(OutBuf, strm.total_out); + OutBytes := strm.total_out; + except + FreeMem(OutBuf); + raise + end; +end; + +procedure DecompressToUserBuf(const InBuf: Pointer; InBytes: Integer; + const OutBuf: Pointer; BufSize: Integer); +var + strm: TZStreamRec; +begin + FillChar(strm, sizeof(strm), 0); + strm.zalloc := zlibAllocMem; + strm.zfree := zlibFreeMem; + strm.next_in := InBuf; + strm.avail_in := InBytes; + strm.next_out := OutBuf; + strm.avail_out := BufSize; + DCheck(inflateInit_(strm, zlib_version, sizeof(strm))); + try + if DCheck(inflate(strm, Z_FINISH)) <> Z_STREAM_END then + raise EZlibError.CreateRes(@sTargetBufferTooSmall); + finally + DCheck(inflateEnd(strm)); + end; +end; + +// TCustomZlibStream + +constructor TCustomZLibStream.Create(Strm: TStream); +begin + inherited Create; + FStrm := Strm; + FStrmPos := Strm.Position; + FZRec.zalloc := zlibAllocMem; + FZRec.zfree := zlibFreeMem; +end; + +procedure TCustomZLibStream.Progress(Sender: TObject); +begin + if Assigned(FOnProgress) then FOnProgress(Sender); +end; + + +// TCompressionStream + +constructor TCompressionStream.Create(CompressionLevel: TCompressionLevel; + Dest: TStream); +const + Levels: array [TCompressionLevel] of ShortInt = + (Z_NO_COMPRESSION, Z_BEST_SPEED, Z_DEFAULT_COMPRESSION, Z_BEST_COMPRESSION); +begin + inherited Create(Dest); + FZRec.next_out := FBuffer; + FZRec.avail_out := sizeof(FBuffer); + CCheck(deflateInit_(FZRec, Levels[CompressionLevel], zlib_version, sizeof(FZRec))); +end; + +destructor TCompressionStream.Destroy; +begin + FZRec.next_in := nil; + FZRec.avail_in := 0; + try + if FStrm.Position <> FStrmPos then FStrm.Position := FStrmPos; + while (CCheck(deflate(FZRec, Z_FINISH)) <> Z_STREAM_END) + and (FZRec.avail_out = 0) do + begin + FStrm.WriteBuffer(FBuffer, sizeof(FBuffer)); + FZRec.next_out := FBuffer; + FZRec.avail_out := sizeof(FBuffer); + end; + if FZRec.avail_out < sizeof(FBuffer) then + FStrm.WriteBuffer(FBuffer, sizeof(FBuffer) - FZRec.avail_out); + finally + deflateEnd(FZRec); + end; + inherited Destroy; +end; + +function TCompressionStream.Read(var Buffer; Count: Longint): Longint; +begin + raise ECompressionError.CreateRes(@sInvalidStreamOp); +end; + +function TCompressionStream.Write(const Buffer; Count: Longint): Longint; +begin + FZRec.next_in := @Buffer; + FZRec.avail_in := Count; + if FStrm.Position <> FStrmPos then FStrm.Position := FStrmPos; + while (FZRec.avail_in > 0) do + begin + CCheck(deflate(FZRec, 0)); + if FZRec.avail_out = 0 then + begin + FStrm.WriteBuffer(FBuffer, sizeof(FBuffer)); + FZRec.next_out := FBuffer; + FZRec.avail_out := sizeof(FBuffer); + FStrmPos := FStrm.Position; + Progress(Self); + end; + end; + Result := Count; +end; + +function TCompressionStream.Seek(Offset: Longint; Origin: Word): Longint; +begin + if (Offset = 0) and (Origin = soFromCurrent) then + Result := FZRec.total_in + else + raise ECompressionError.CreateRes(@sInvalidStreamOp); +end; + +function TCompressionStream.GetCompressionRate: Single; +begin + if FZRec.total_in = 0 then + Result := 0 + else + Result := (1.0 - (FZRec.total_out / FZRec.total_in)) * 100.0; +end; + + +// TDecompressionStream + +constructor TDecompressionStream.Create(Source: TStream); +begin + inherited Create(Source); + FZRec.next_in := FBuffer; + FZRec.avail_in := 0; + DCheck(inflateInit_(FZRec, zlib_version, sizeof(FZRec))); +end; + +destructor TDecompressionStream.Destroy; +begin + FStrm.Seek(-FZRec.avail_in, 1); + inflateEnd(FZRec); + inherited Destroy; +end; + +function TDecompressionStream.Read(var Buffer; Count: Longint): Longint; +begin + FZRec.next_out := @Buffer; + FZRec.avail_out := Count; + if FStrm.Position <> FStrmPos then FStrm.Position := FStrmPos; + while (FZRec.avail_out > 0) do + begin + if FZRec.avail_in = 0 then + begin + FZRec.avail_in := FStrm.Read(FBuffer, sizeof(FBuffer)); + if FZRec.avail_in = 0 then + begin + Result := Count - FZRec.avail_out; + Exit; + end; + FZRec.next_in := FBuffer; + FStrmPos := FStrm.Position; + Progress(Self); + end; + CCheck(inflate(FZRec, 0)); + end; + Result := Count; +end; + +function TDecompressionStream.Write(const Buffer; Count: Longint): Longint; +begin + raise EDecompressionError.CreateRes(@sInvalidStreamOp); +end; + +function TDecompressionStream.Seek(Offset: Longint; Origin: Word): Longint; +var + I: Integer; + Buf: array [0..4095] of Char; +begin + if (Offset = 0) and (Origin = soFromBeginning) then + begin + DCheck(inflateReset(FZRec)); + FZRec.next_in := FBuffer; + FZRec.avail_in := 0; + FStrm.Position := 0; + FStrmPos := 0; + end + else if ( (Offset >= 0) and (Origin = soFromCurrent)) or + ( ((Offset - FZRec.total_out) > 0) and (Origin = soFromBeginning)) then + begin + if Origin = soFromBeginning then Dec(Offset, FZRec.total_out); + if Offset > 0 then + begin + for I := 1 to Offset div sizeof(Buf) do + ReadBuffer(Buf, sizeof(Buf)); + ReadBuffer(Buf, Offset mod sizeof(Buf)); + end; + end + else + raise EDecompressionError.CreateRes(@sInvalidStreamOp); + Result := FZRec.total_out; +end; + + +end. diff --git a/compat/zlib/contrib/delphi/ZLibConst.pas b/compat/zlib/contrib/delphi/ZLibConst.pas new file mode 100644 index 0000000..cdfe136 --- /dev/null +++ b/compat/zlib/contrib/delphi/ZLibConst.pas @@ -0,0 +1,11 @@ +unit ZLibConst; + +interface + +resourcestring + sTargetBufferTooSmall = 'ZLib error: target buffer may be too small'; + sInvalidStreamOp = 'Invalid stream operation'; + +implementation + +end. diff --git a/compat/zlib/contrib/delphi/readme.txt b/compat/zlib/contrib/delphi/readme.txt new file mode 100644 index 0000000..2dc9a8b --- /dev/null +++ b/compat/zlib/contrib/delphi/readme.txt @@ -0,0 +1,76 @@ + +Overview +======== + +This directory contains an update to the ZLib interface unit, +distributed by Borland as a Delphi supplemental component. + +The original ZLib unit is Copyright (c) 1997,99 Borland Corp., +and is based on zlib version 1.0.4. There are a series of bugs +and security problems associated with that old zlib version, and +we recommend the users to update their ZLib unit. + + +Summary of modifications +======================== + +- Improved makefile, adapted to zlib version 1.2.1. + +- Some field types from TZStreamRec are changed from Integer to + Longint, for consistency with the zlib.h header, and for 64-bit + readiness. + +- The zlib_version constant is updated. + +- The new Z_RLE strategy has its corresponding symbolic constant. + +- The allocation and deallocation functions and function types + (TAlloc, TFree, zlibAllocMem and zlibFreeMem) are now cdecl, + and _malloc and _free are added as C RTL stubs. As a result, + the original C sources of zlib can be compiled out of the box, + and linked to the ZLib unit. + + +Suggestions for improvements +============================ + +Currently, the ZLib unit provides only a limited wrapper around +the zlib library, and much of the original zlib functionality is +missing. Handling compressed file formats like ZIP/GZIP or PNG +cannot be implemented without having this functionality. +Applications that handle these formats are either using their own, +duplicated code, or not using the ZLib unit at all. + +Here are a few suggestions: + +- Checksum class wrappers around adler32() and crc32(), similar + to the Java classes that implement the java.util.zip.Checksum + interface. + +- The ability to read and write raw deflate streams, without the + zlib stream header and trailer. Raw deflate streams are used + in the ZIP file format. + +- The ability to read and write gzip streams, used in the GZIP + file format, and normally produced by the gzip program. + +- The ability to select a different compression strategy, useful + to PNG and MNG image compression, and to multimedia compression + in general. Besides the compression level + + TCompressionLevel = (clNone, clFastest, clDefault, clMax); + + which, in fact, could have used the 'z' prefix and avoided + TColor-like symbols + + TCompressionLevel = (zcNone, zcFastest, zcDefault, zcMax); + + there could be a compression strategy + + TCompressionStrategy = (zsDefault, zsFiltered, zsHuffmanOnly, zsRle); + +- ZIP and GZIP stream handling via TStreams. + + +-- +Cosmin Truta diff --git a/compat/zlib/contrib/delphi/zlibd32.mak b/compat/zlib/contrib/delphi/zlibd32.mak new file mode 100644 index 0000000..9bb00b7 --- /dev/null +++ b/compat/zlib/contrib/delphi/zlibd32.mak @@ -0,0 +1,99 @@ +# Makefile for zlib +# For use with Delphi and C++ Builder under Win32 +# Updated for zlib 1.2.x by Cosmin Truta + +# ------------ Borland C++ ------------ + +# This project uses the Delphi (fastcall/register) calling convention: +LOC = -DZEXPORT=__fastcall -DZEXPORTVA=__cdecl + +CC = bcc32 +LD = bcc32 +AR = tlib +# do not use "-pr" in CFLAGS +CFLAGS = -a -d -k- -O2 $(LOC) +LDFLAGS = + + +# variables +ZLIB_LIB = zlib.lib + +OBJ1 = adler32.obj compress.obj crc32.obj deflate.obj gzclose.obj gzlib.obj gzread.obj +OBJ2 = gzwrite.obj infback.obj inffast.obj inflate.obj inftrees.obj trees.obj uncompr.obj zutil.obj +OBJP1 = +adler32.obj+compress.obj+crc32.obj+deflate.obj+gzclose.obj+gzlib.obj+gzread.obj +OBJP2 = +gzwrite.obj+infback.obj+inffast.obj+inflate.obj+inftrees.obj+trees.obj+uncompr.obj+zutil.obj + + +# targets +all: $(ZLIB_LIB) example.exe minigzip.exe + +.c.obj: + $(CC) -c $(CFLAGS) $*.c + +adler32.obj: adler32.c zlib.h zconf.h + +compress.obj: compress.c zlib.h zconf.h + +crc32.obj: crc32.c zlib.h zconf.h crc32.h + +deflate.obj: deflate.c deflate.h zutil.h zlib.h zconf.h + +gzclose.obj: gzclose.c zlib.h zconf.h gzguts.h + +gzlib.obj: gzlib.c zlib.h zconf.h gzguts.h + +gzread.obj: gzread.c zlib.h zconf.h gzguts.h + +gzwrite.obj: gzwrite.c zlib.h zconf.h gzguts.h + +infback.obj: infback.c zutil.h zlib.h zconf.h inftrees.h inflate.h \ + inffast.h inffixed.h + +inffast.obj: inffast.c zutil.h zlib.h zconf.h inftrees.h inflate.h \ + inffast.h + +inflate.obj: inflate.c zutil.h zlib.h zconf.h inftrees.h inflate.h \ + inffast.h inffixed.h + +inftrees.obj: inftrees.c zutil.h zlib.h zconf.h inftrees.h + +trees.obj: trees.c zutil.h zlib.h zconf.h deflate.h trees.h + +uncompr.obj: uncompr.c zlib.h zconf.h + +zutil.obj: zutil.c zutil.h zlib.h zconf.h + +example.obj: test/example.c zlib.h zconf.h + +minigzip.obj: test/minigzip.c zlib.h zconf.h + + +# For the sake of the old Borland make, +# the command line is cut to fit in the MS-DOS 128 byte limit: +$(ZLIB_LIB): $(OBJ1) $(OBJ2) + -del $(ZLIB_LIB) + $(AR) $(ZLIB_LIB) $(OBJP1) + $(AR) $(ZLIB_LIB) $(OBJP2) + + +# testing +test: example.exe minigzip.exe + example + echo hello world | minigzip | minigzip -d + +example.exe: example.obj $(ZLIB_LIB) + $(LD) $(LDFLAGS) example.obj $(ZLIB_LIB) + +minigzip.exe: minigzip.obj $(ZLIB_LIB) + $(LD) $(LDFLAGS) minigzip.obj $(ZLIB_LIB) + + +# cleanup +clean: + -del *.obj + -del *.exe + -del *.lib + -del *.tds + -del zlib.bak + -del foo.gz + diff --git a/compat/zlib/contrib/dotzlib/DotZLib.build b/compat/zlib/contrib/dotzlib/DotZLib.build new file mode 100644 index 0000000..e69630c --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib.build @@ -0,0 +1,33 @@ + + + A .Net wrapper library around ZLib1.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/compat/zlib/contrib/dotzlib/DotZLib.chm b/compat/zlib/contrib/dotzlib/DotZLib.chm new file mode 100644 index 0000000..f214a44 Binary files /dev/null and b/compat/zlib/contrib/dotzlib/DotZLib.chm differ diff --git a/compat/zlib/contrib/dotzlib/DotZLib.sln b/compat/zlib/contrib/dotzlib/DotZLib.sln new file mode 100644 index 0000000..5d533d6 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib.sln @@ -0,0 +1,21 @@ +Microsoft Visual Studio Solution File, Format Version 8.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotZLib", "DotZLib\DotZLib.csproj", "{BB1EE0B1-1808-46CB-B786-949D91117FC5}" + ProjectSection(ProjectDependencies) = postProject + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfiguration) = preSolution + Debug = Debug + Release = Release + EndGlobalSection + GlobalSection(ProjectConfiguration) = postSolution + {BB1EE0B1-1808-46CB-B786-949D91117FC5}.Debug.ActiveCfg = Debug|.NET + {BB1EE0B1-1808-46CB-B786-949D91117FC5}.Debug.Build.0 = Debug|.NET + {BB1EE0B1-1808-46CB-B786-949D91117FC5}.Release.ActiveCfg = Release|.NET + {BB1EE0B1-1808-46CB-B786-949D91117FC5}.Release.Build.0 = Release|.NET + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + EndGlobalSection + GlobalSection(ExtensibilityAddIns) = postSolution + EndGlobalSection +EndGlobal diff --git a/compat/zlib/contrib/dotzlib/DotZLib/AssemblyInfo.cs b/compat/zlib/contrib/dotzlib/DotZLib/AssemblyInfo.cs new file mode 100644 index 0000000..724c534 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib/AssemblyInfo.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +// +[assembly: AssemblyTitle("DotZLib")] +[assembly: AssemblyDescription(".Net bindings for ZLib compression dll 1.2.x")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Henrik Ravn")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("(c) 2004 by Henrik Ravn")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: + +[assembly: AssemblyVersion("1.0.*")] + +// +// In order to sign your assembly you must specify a key to use. Refer to the +// Microsoft .NET Framework documentation for more information on assembly signing. +// +// Use the attributes below to control which key is used for signing. +// +// Notes: +// (*) If no key is specified, the assembly is not signed. +// (*) KeyName refers to a key that has been installed in the Crypto Service +// Provider (CSP) on your machine. KeyFile refers to a file which contains +// a key. +// (*) If the KeyFile and the KeyName values are both specified, the +// following processing occurs: +// (1) If the KeyName can be found in the CSP, that key is used. +// (2) If the KeyName does not exist and the KeyFile does exist, the key +// in the KeyFile is installed into the CSP and used. +// (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility. +// When specifying the KeyFile, the location of the KeyFile should be +// relative to the project output directory which is +// %Project Directory%\obj\. For example, if your KeyFile is +// located in the project directory, you would specify the AssemblyKeyFile +// attribute as [assembly: AssemblyKeyFile("..\\..\\mykey.snk")] +// (*) Delay Signing is an advanced option - see the Microsoft .NET Framework +// documentation for more information on this. +// +[assembly: AssemblyDelaySign(false)] +[assembly: AssemblyKeyFile("")] +[assembly: AssemblyKeyName("")] diff --git a/compat/zlib/contrib/dotzlib/DotZLib/ChecksumImpl.cs b/compat/zlib/contrib/dotzlib/DotZLib/ChecksumImpl.cs new file mode 100644 index 0000000..c885167 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib/ChecksumImpl.cs @@ -0,0 +1,202 @@ +// +// © Copyright Henrik Ravn 2004 +// +// Use, modification and distribution are subject to the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.Runtime.InteropServices; +using System.Text; + + +namespace DotZLib +{ + #region ChecksumGeneratorBase + /// + /// Implements the common functionality needed for all s + /// + /// + public abstract class ChecksumGeneratorBase : ChecksumGenerator + { + /// + /// The value of the current checksum + /// + protected uint _current; + + /// + /// Initializes a new instance of the checksum generator base - the current checksum is + /// set to zero + /// + public ChecksumGeneratorBase() + { + _current = 0; + } + + /// + /// Initializes a new instance of the checksum generator base with a specified value + /// + /// The value to set the current checksum to + public ChecksumGeneratorBase(uint initialValue) + { + _current = initialValue; + } + + /// + /// Resets the current checksum to zero + /// + public void Reset() { _current = 0; } + + /// + /// Gets the current checksum value + /// + public uint Value { get { return _current; } } + + /// + /// Updates the current checksum with part of an array of bytes + /// + /// The data to update the checksum with + /// Where in data to start updating + /// The number of bytes from data to use + /// The sum of offset and count is larger than the length of data + /// data is a null reference + /// Offset or count is negative. + /// All the other Update methods are implemented in terms of this one. + /// This is therefore the only method a derived class has to implement + public abstract void Update(byte[] data, int offset, int count); + + /// + /// Updates the current checksum with an array of bytes. + /// + /// The data to update the checksum with + public void Update(byte[] data) + { + Update(data, 0, data.Length); + } + + /// + /// Updates the current checksum with the data from a string + /// + /// The string to update the checksum with + /// The characters in the string are converted by the UTF-8 encoding + public void Update(string data) + { + Update(Encoding.UTF8.GetBytes(data)); + } + + /// + /// Updates the current checksum with the data from a string, using a specific encoding + /// + /// The string to update the checksum with + /// The encoding to use + public void Update(string data, Encoding encoding) + { + Update(encoding.GetBytes(data)); + } + + } + #endregion + + #region CRC32 + /// + /// Implements a CRC32 checksum generator + /// + public sealed class CRC32Checksum : ChecksumGeneratorBase + { + #region DLL imports + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern uint crc32(uint crc, int data, uint length); + + #endregion + + /// + /// Initializes a new instance of the CRC32 checksum generator + /// + public CRC32Checksum() : base() {} + + /// + /// Initializes a new instance of the CRC32 checksum generator with a specified value + /// + /// The value to set the current checksum to + public CRC32Checksum(uint initialValue) : base(initialValue) {} + + /// + /// Updates the current checksum with part of an array of bytes + /// + /// The data to update the checksum with + /// Where in data to start updating + /// The number of bytes from data to use + /// The sum of offset and count is larger than the length of data + /// data is a null reference + /// Offset or count is negative. + public override void Update(byte[] data, int offset, int count) + { + if (offset < 0 || count < 0) throw new ArgumentOutOfRangeException(); + if ((offset+count) > data.Length) throw new ArgumentException(); + GCHandle hData = GCHandle.Alloc(data, GCHandleType.Pinned); + try + { + _current = crc32(_current, hData.AddrOfPinnedObject().ToInt32()+offset, (uint)count); + } + finally + { + hData.Free(); + } + } + + } + #endregion + + #region Adler + /// + /// Implements a checksum generator that computes the Adler checksum on data + /// + public sealed class AdlerChecksum : ChecksumGeneratorBase + { + #region DLL imports + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern uint adler32(uint adler, int data, uint length); + + #endregion + + /// + /// Initializes a new instance of the Adler checksum generator + /// + public AdlerChecksum() : base() {} + + /// + /// Initializes a new instance of the Adler checksum generator with a specified value + /// + /// The value to set the current checksum to + public AdlerChecksum(uint initialValue) : base(initialValue) {} + + /// + /// Updates the current checksum with part of an array of bytes + /// + /// The data to update the checksum with + /// Where in data to start updating + /// The number of bytes from data to use + /// The sum of offset and count is larger than the length of data + /// data is a null reference + /// Offset or count is negative. + public override void Update(byte[] data, int offset, int count) + { + if (offset < 0 || count < 0) throw new ArgumentOutOfRangeException(); + if ((offset+count) > data.Length) throw new ArgumentException(); + GCHandle hData = GCHandle.Alloc(data, GCHandleType.Pinned); + try + { + _current = adler32(_current, hData.AddrOfPinnedObject().ToInt32()+offset, (uint)count); + } + finally + { + hData.Free(); + } + } + + } + #endregion + +} \ No newline at end of file diff --git a/compat/zlib/contrib/dotzlib/DotZLib/CircularBuffer.cs b/compat/zlib/contrib/dotzlib/DotZLib/CircularBuffer.cs new file mode 100644 index 0000000..b9b71d3 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib/CircularBuffer.cs @@ -0,0 +1,83 @@ +// +// © Copyright Henrik Ravn 2004 +// +// Use, modification and distribution are subject to the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.Diagnostics; + +namespace DotZLib +{ + + /// + /// This class implements a circular buffer + /// + internal class CircularBuffer + { + #region Private data + private int _capacity; + private int _head; + private int _tail; + private int _size; + private byte[] _buffer; + #endregion + + public CircularBuffer(int capacity) + { + Debug.Assert( capacity > 0 ); + _buffer = new byte[capacity]; + _capacity = capacity; + _head = 0; + _tail = 0; + _size = 0; + } + + public int Size { get { return _size; } } + + public int Put(byte[] source, int offset, int count) + { + Debug.Assert( count > 0 ); + int trueCount = Math.Min(count, _capacity - Size); + for (int i = 0; i < trueCount; ++i) + _buffer[(_tail+i) % _capacity] = source[offset+i]; + _tail += trueCount; + _tail %= _capacity; + _size += trueCount; + return trueCount; + } + + public bool Put(byte b) + { + if (Size == _capacity) // no room + return false; + _buffer[_tail++] = b; + _tail %= _capacity; + ++_size; + return true; + } + + public int Get(byte[] destination, int offset, int count) + { + int trueCount = Math.Min(count,Size); + for (int i = 0; i < trueCount; ++i) + destination[offset + i] = _buffer[(_head+i) % _capacity]; + _head += trueCount; + _head %= _capacity; + _size -= trueCount; + return trueCount; + } + + public int Get() + { + if (Size == 0) + return -1; + + int result = (int)_buffer[_head++ % _capacity]; + --_size; + return result; + } + + } +} diff --git a/compat/zlib/contrib/dotzlib/DotZLib/CodecBase.cs b/compat/zlib/contrib/dotzlib/DotZLib/CodecBase.cs new file mode 100644 index 0000000..7ce8fa4 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib/CodecBase.cs @@ -0,0 +1,198 @@ +// +// © Copyright Henrik Ravn 2004 +// +// Use, modification and distribution are subject to the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.Runtime.InteropServices; + +namespace DotZLib +{ + /// + /// Implements the common functionality needed for all s + /// + public abstract class CodecBase : Codec, IDisposable + { + + #region Data members + + /// + /// Instance of the internal zlib buffer structure that is + /// passed to all functions in the zlib dll + /// + internal ZStream _ztream = new ZStream(); + + /// + /// True if the object instance has been disposed, false otherwise + /// + protected bool _isDisposed = false; + + /// + /// The size of the internal buffers + /// + protected const int kBufferSize = 16384; + + private byte[] _outBuffer = new byte[kBufferSize]; + private byte[] _inBuffer = new byte[kBufferSize]; + + private GCHandle _hInput; + private GCHandle _hOutput; + + private uint _checksum = 0; + + #endregion + + /// + /// Initializes a new instance of the CodeBase class. + /// + public CodecBase() + { + try + { + _hInput = GCHandle.Alloc(_inBuffer, GCHandleType.Pinned); + _hOutput = GCHandle.Alloc(_outBuffer, GCHandleType.Pinned); + } + catch (Exception) + { + CleanUp(false); + throw; + } + } + + + #region Codec Members + + /// + /// Occurs when more processed data are available. + /// + public event DataAvailableHandler DataAvailable; + + /// + /// Fires the event + /// + protected void OnDataAvailable() + { + if (_ztream.total_out > 0) + { + if (DataAvailable != null) + DataAvailable( _outBuffer, 0, (int)_ztream.total_out); + resetOutput(); + } + } + + /// + /// Adds more data to the codec to be processed. + /// + /// Byte array containing the data to be added to the codec + /// Adding data may, or may not, raise the DataAvailable event + public void Add(byte[] data) + { + Add(data,0,data.Length); + } + + /// + /// Adds more data to the codec to be processed. + /// + /// Byte array containing the data to be added to the codec + /// The index of the first byte to add from data + /// The number of bytes to add + /// Adding data may, or may not, raise the DataAvailable event + /// This must be implemented by a derived class + public abstract void Add(byte[] data, int offset, int count); + + /// + /// Finishes up any pending data that needs to be processed and handled. + /// + /// This must be implemented by a derived class + public abstract void Finish(); + + /// + /// Gets the checksum of the data that has been added so far + /// + public uint Checksum { get { return _checksum; } } + + #endregion + + #region Destructor & IDisposable stuff + + /// + /// Destroys this instance + /// + ~CodecBase() + { + CleanUp(false); + } + + /// + /// Releases any unmanaged resources and calls the method of the derived class + /// + public void Dispose() + { + CleanUp(true); + } + + /// + /// Performs any codec specific cleanup + /// + /// This must be implemented by a derived class + protected abstract void CleanUp(); + + // performs the release of the handles and calls the derived CleanUp() + private void CleanUp(bool isDisposing) + { + if (!_isDisposed) + { + CleanUp(); + if (_hInput.IsAllocated) + _hInput.Free(); + if (_hOutput.IsAllocated) + _hOutput.Free(); + + _isDisposed = true; + } + } + + + #endregion + + #region Helper methods + + /// + /// Copies a number of bytes to the internal codec buffer - ready for processing + /// + /// The byte array that contains the data to copy + /// The index of the first byte to copy + /// The number of bytes to copy from data + protected void copyInput(byte[] data, int startIndex, int count) + { + Array.Copy(data, startIndex, _inBuffer,0, count); + _ztream.next_in = _hInput.AddrOfPinnedObject(); + _ztream.total_in = 0; + _ztream.avail_in = (uint)count; + + } + + /// + /// Resets the internal output buffers to a known state - ready for processing + /// + protected void resetOutput() + { + _ztream.total_out = 0; + _ztream.avail_out = kBufferSize; + _ztream.next_out = _hOutput.AddrOfPinnedObject(); + } + + /// + /// Updates the running checksum property + /// + /// The new checksum value + protected void setChecksum(uint newSum) + { + _checksum = newSum; + } + #endregion + + } +} diff --git a/compat/zlib/contrib/dotzlib/DotZLib/Deflater.cs b/compat/zlib/contrib/dotzlib/DotZLib/Deflater.cs new file mode 100644 index 0000000..4665995 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib/Deflater.cs @@ -0,0 +1,106 @@ +// +// © Copyright Henrik Ravn 2004 +// +// Use, modification and distribution are subject to the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace DotZLib +{ + + /// + /// Implements a data compressor, using the deflate algorithm in the ZLib dll + /// + public sealed class Deflater : CodecBase + { + #region Dll imports + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Ansi)] + private static extern int deflateInit_(ref ZStream sz, int level, string vs, int size); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int deflate(ref ZStream sz, int flush); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int deflateReset(ref ZStream sz); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int deflateEnd(ref ZStream sz); + #endregion + + /// + /// Constructs an new instance of the Deflater + /// + /// The compression level to use for this Deflater + public Deflater(CompressLevel level) : base() + { + int retval = deflateInit_(ref _ztream, (int)level, Info.Version, Marshal.SizeOf(_ztream)); + if (retval != 0) + throw new ZLibException(retval, "Could not initialize deflater"); + + resetOutput(); + } + + /// + /// Adds more data to the codec to be processed. + /// + /// Byte array containing the data to be added to the codec + /// The index of the first byte to add from data + /// The number of bytes to add + /// Adding data may, or may not, raise the DataAvailable event + public override void Add(byte[] data, int offset, int count) + { + if (data == null) throw new ArgumentNullException(); + if (offset < 0 || count < 0) throw new ArgumentOutOfRangeException(); + if ((offset+count) > data.Length) throw new ArgumentException(); + + int total = count; + int inputIndex = offset; + int err = 0; + + while (err >= 0 && inputIndex < total) + { + copyInput(data, inputIndex, Math.Min(total - inputIndex, kBufferSize)); + while (err >= 0 && _ztream.avail_in > 0) + { + err = deflate(ref _ztream, (int)FlushTypes.None); + if (err == 0) + while (_ztream.avail_out == 0) + { + OnDataAvailable(); + err = deflate(ref _ztream, (int)FlushTypes.None); + } + inputIndex += (int)_ztream.total_in; + } + } + setChecksum( _ztream.adler ); + } + + + /// + /// Finishes up any pending data that needs to be processed and handled. + /// + public override void Finish() + { + int err; + do + { + err = deflate(ref _ztream, (int)FlushTypes.Finish); + OnDataAvailable(); + } + while (err == 0); + setChecksum( _ztream.adler ); + deflateReset(ref _ztream); + resetOutput(); + } + + /// + /// Closes the internal zlib deflate stream + /// + protected override void CleanUp() { deflateEnd(ref _ztream); } + + } +} diff --git a/compat/zlib/contrib/dotzlib/DotZLib/DotZLib.cs b/compat/zlib/contrib/dotzlib/DotZLib/DotZLib.cs new file mode 100644 index 0000000..c2462c2 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib/DotZLib.cs @@ -0,0 +1,288 @@ +// +// © Copyright Henrik Ravn 2004 +// +// Use, modification and distribution are subject to the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + + +namespace DotZLib +{ + + #region Internal types + + /// + /// Defines constants for the various flush types used with zlib + /// + internal enum FlushTypes + { + None, Partial, Sync, Full, Finish, Block + } + + #region ZStream structure + // internal mapping of the zlib zstream structure for marshalling + [StructLayoutAttribute(LayoutKind.Sequential, Pack=4, Size=0, CharSet=CharSet.Ansi)] + internal struct ZStream + { + public IntPtr next_in; + public uint avail_in; + public uint total_in; + + public IntPtr next_out; + public uint avail_out; + public uint total_out; + + [MarshalAs(UnmanagedType.LPStr)] + string msg; + uint state; + + uint zalloc; + uint zfree; + uint opaque; + + int data_type; + public uint adler; + uint reserved; + } + + #endregion + + #endregion + + #region Public enums + /// + /// Defines constants for the available compression levels in zlib + /// + public enum CompressLevel : int + { + /// + /// The default compression level with a reasonable compromise between compression and speed + /// + Default = -1, + /// + /// No compression at all. The data are passed straight through. + /// + None = 0, + /// + /// The maximum compression rate available. + /// + Best = 9, + /// + /// The fastest available compression level. + /// + Fastest = 1 + } + #endregion + + #region Exception classes + /// + /// The exception that is thrown when an error occurs on the zlib dll + /// + public class ZLibException : ApplicationException + { + /// + /// Initializes a new instance of the class with a specified + /// error message and error code + /// + /// The zlib error code that caused the exception + /// A message that (hopefully) describes the error + public ZLibException(int errorCode, string msg) : base(String.Format("ZLib error {0} {1}", errorCode, msg)) + { + } + + /// + /// Initializes a new instance of the class with a specified + /// error code + /// + /// The zlib error code that caused the exception + public ZLibException(int errorCode) : base(String.Format("ZLib error {0}", errorCode)) + { + } + } + #endregion + + #region Interfaces + + /// + /// Declares methods and properties that enables a running checksum to be calculated + /// + public interface ChecksumGenerator + { + /// + /// Gets the current value of the checksum + /// + uint Value { get; } + + /// + /// Clears the current checksum to 0 + /// + void Reset(); + + /// + /// Updates the current checksum with an array of bytes + /// + /// The data to update the checksum with + void Update(byte[] data); + + /// + /// Updates the current checksum with part of an array of bytes + /// + /// The data to update the checksum with + /// Where in data to start updating + /// The number of bytes from data to use + /// The sum of offset and count is larger than the length of data + /// data is a null reference + /// Offset or count is negative. + void Update(byte[] data, int offset, int count); + + /// + /// Updates the current checksum with the data from a string + /// + /// The string to update the checksum with + /// The characters in the string are converted by the UTF-8 encoding + void Update(string data); + + /// + /// Updates the current checksum with the data from a string, using a specific encoding + /// + /// The string to update the checksum with + /// The encoding to use + void Update(string data, Encoding encoding); + } + + + /// + /// Represents the method that will be called from a codec when new data + /// are available. + /// + /// The byte array containing the processed data + /// The index of the first processed byte in data + /// The number of processed bytes available + /// On return from this method, the data may be overwritten, so grab it while you can. + /// You cannot assume that startIndex will be zero. + /// + public delegate void DataAvailableHandler(byte[] data, int startIndex, int count); + + /// + /// Declares methods and events for implementing compressors/decompressors + /// + public interface Codec + { + /// + /// Occurs when more processed data are available. + /// + event DataAvailableHandler DataAvailable; + + /// + /// Adds more data to the codec to be processed. + /// + /// Byte array containing the data to be added to the codec + /// Adding data may, or may not, raise the DataAvailable event + void Add(byte[] data); + + /// + /// Adds more data to the codec to be processed. + /// + /// Byte array containing the data to be added to the codec + /// The index of the first byte to add from data + /// The number of bytes to add + /// Adding data may, or may not, raise the DataAvailable event + void Add(byte[] data, int offset, int count); + + /// + /// Finishes up any pending data that needs to be processed and handled. + /// + void Finish(); + + /// + /// Gets the checksum of the data that has been added so far + /// + uint Checksum { get; } + + + } + + #endregion + + #region Classes + /// + /// Encapsulates general information about the ZLib library + /// + public class Info + { + #region DLL imports + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern uint zlibCompileFlags(); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern string zlibVersion(); + #endregion + + #region Private stuff + private uint _flags; + + // helper function that unpacks a bitsize mask + private static int bitSize(uint bits) + { + switch (bits) + { + case 0: return 16; + case 1: return 32; + case 2: return 64; + } + return -1; + } + #endregion + + /// + /// Constructs an instance of the Info class. + /// + public Info() + { + _flags = zlibCompileFlags(); + } + + /// + /// True if the library is compiled with debug info + /// + public bool HasDebugInfo { get { return 0 != (_flags & 0x100); } } + + /// + /// True if the library is compiled with assembly optimizations + /// + public bool UsesAssemblyCode { get { return 0 != (_flags & 0x200); } } + + /// + /// Gets the size of the unsigned int that was compiled into Zlib + /// + public int SizeOfUInt { get { return bitSize(_flags & 3); } } + + /// + /// Gets the size of the unsigned long that was compiled into Zlib + /// + public int SizeOfULong { get { return bitSize((_flags >> 2) & 3); } } + + /// + /// Gets the size of the pointers that were compiled into Zlib + /// + public int SizeOfPointer { get { return bitSize((_flags >> 4) & 3); } } + + /// + /// Gets the size of the z_off_t type that was compiled into Zlib + /// + public int SizeOfOffset { get { return bitSize((_flags >> 6) & 3); } } + + /// + /// Gets the version of ZLib as a string, e.g. "1.2.1" + /// + public static string Version { get { return zlibVersion(); } } + } + + #endregion + +} diff --git a/compat/zlib/contrib/dotzlib/DotZLib/DotZLib.csproj b/compat/zlib/contrib/dotzlib/DotZLib/DotZLib.csproj new file mode 100644 index 0000000..dea7fb1 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib/DotZLib.csproj @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/compat/zlib/contrib/dotzlib/DotZLib/GZipStream.cs b/compat/zlib/contrib/dotzlib/DotZLib/GZipStream.cs new file mode 100644 index 0000000..bfb1772 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib/GZipStream.cs @@ -0,0 +1,301 @@ +// +// © Copyright Henrik Ravn 2004 +// +// Use, modification and distribution are subject to the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace DotZLib +{ + /// + /// Implements a compressed , in GZip (.gz) format. + /// + public class GZipStream : Stream, IDisposable + { + #region Dll Imports + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Ansi)] + private static extern IntPtr gzopen(string name, string mode); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int gzclose(IntPtr gzFile); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int gzwrite(IntPtr gzFile, int data, int length); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int gzread(IntPtr gzFile, int data, int length); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int gzgetc(IntPtr gzFile); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int gzputc(IntPtr gzFile, int c); + + #endregion + + #region Private data + private IntPtr _gzFile; + private bool _isDisposed = false; + private bool _isWriting; + #endregion + + #region Constructors + /// + /// Creates a new file as a writeable GZipStream + /// + /// The name of the compressed file to create + /// The compression level to use when adding data + /// If an error occurred in the internal zlib function + public GZipStream(string fileName, CompressLevel level) + { + _isWriting = true; + _gzFile = gzopen(fileName, String.Format("wb{0}", (int)level)); + if (_gzFile == IntPtr.Zero) + throw new ZLibException(-1, "Could not open " + fileName); + } + + /// + /// Opens an existing file as a readable GZipStream + /// + /// The name of the file to open + /// If an error occurred in the internal zlib function + public GZipStream(string fileName) + { + _isWriting = false; + _gzFile = gzopen(fileName, "rb"); + if (_gzFile == IntPtr.Zero) + throw new ZLibException(-1, "Could not open " + fileName); + + } + #endregion + + #region Access properties + /// + /// Returns true of this stream can be read from, false otherwise + /// + public override bool CanRead + { + get + { + return !_isWriting; + } + } + + + /// + /// Returns false. + /// + public override bool CanSeek + { + get + { + return false; + } + } + + /// + /// Returns true if this tsream is writeable, false otherwise + /// + public override bool CanWrite + { + get + { + return _isWriting; + } + } + #endregion + + #region Destructor & IDispose stuff + + /// + /// Destroys this instance + /// + ~GZipStream() + { + cleanUp(false); + } + + /// + /// Closes the external file handle + /// + public void Dispose() + { + cleanUp(true); + } + + // Does the actual closing of the file handle. + private void cleanUp(bool isDisposing) + { + if (!_isDisposed) + { + gzclose(_gzFile); + _isDisposed = true; + } + } + #endregion + + #region Basic reading and writing + /// + /// Attempts to read a number of bytes from the stream. + /// + /// The destination data buffer + /// The index of the first destination byte in buffer + /// The number of bytes requested + /// The number of bytes read + /// If buffer is null + /// If count or offset are negative + /// If offset + count is > buffer.Length + /// If this stream is not readable. + /// If this stream has been disposed. + public override int Read(byte[] buffer, int offset, int count) + { + if (!CanRead) throw new NotSupportedException(); + if (buffer == null) throw new ArgumentNullException(); + if (offset < 0 || count < 0) throw new ArgumentOutOfRangeException(); + if ((offset+count) > buffer.Length) throw new ArgumentException(); + if (_isDisposed) throw new ObjectDisposedException("GZipStream"); + + GCHandle h = GCHandle.Alloc(buffer, GCHandleType.Pinned); + int result; + try + { + result = gzread(_gzFile, h.AddrOfPinnedObject().ToInt32() + offset, count); + if (result < 0) + throw new IOException(); + } + finally + { + h.Free(); + } + return result; + } + + /// + /// Attempts to read a single byte from the stream. + /// + /// The byte that was read, or -1 in case of error or End-Of-File + public override int ReadByte() + { + if (!CanRead) throw new NotSupportedException(); + if (_isDisposed) throw new ObjectDisposedException("GZipStream"); + return gzgetc(_gzFile); + } + + /// + /// Writes a number of bytes to the stream + /// + /// + /// + /// + /// If buffer is null + /// If count or offset are negative + /// If offset + count is > buffer.Length + /// If this stream is not writeable. + /// If this stream has been disposed. + public override void Write(byte[] buffer, int offset, int count) + { + if (!CanWrite) throw new NotSupportedException(); + if (buffer == null) throw new ArgumentNullException(); + if (offset < 0 || count < 0) throw new ArgumentOutOfRangeException(); + if ((offset+count) > buffer.Length) throw new ArgumentException(); + if (_isDisposed) throw new ObjectDisposedException("GZipStream"); + + GCHandle h = GCHandle.Alloc(buffer, GCHandleType.Pinned); + try + { + int result = gzwrite(_gzFile, h.AddrOfPinnedObject().ToInt32() + offset, count); + if (result < 0) + throw new IOException(); + } + finally + { + h.Free(); + } + } + + /// + /// Writes a single byte to the stream + /// + /// The byte to add to the stream. + /// If this stream is not writeable. + /// If this stream has been disposed. + public override void WriteByte(byte value) + { + if (!CanWrite) throw new NotSupportedException(); + if (_isDisposed) throw new ObjectDisposedException("GZipStream"); + + int result = gzputc(_gzFile, (int)value); + if (result < 0) + throw new IOException(); + } + #endregion + + #region Position & length stuff + /// + /// Not supported. + /// + /// + /// Always thrown + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Not supported. + /// + /// + /// + /// + /// Always thrown + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + /// Flushes the GZipStream. + /// + /// In this implementation, this method does nothing. This is because excessive + /// flushing may degrade the achievable compression rates. + public override void Flush() + { + // left empty on purpose + } + + /// + /// Gets/sets the current position in the GZipStream. Not supported. + /// + /// In this implementation this property is not supported + /// Always thrown + public override long Position + { + get + { + throw new NotSupportedException(); + } + set + { + throw new NotSupportedException(); + } + } + + /// + /// Gets the size of the stream. Not supported. + /// + /// In this implementation this property is not supported + /// Always thrown + public override long Length + { + get + { + throw new NotSupportedException(); + } + } + #endregion + } +} diff --git a/compat/zlib/contrib/dotzlib/DotZLib/Inflater.cs b/compat/zlib/contrib/dotzlib/DotZLib/Inflater.cs new file mode 100644 index 0000000..88f7c17 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib/Inflater.cs @@ -0,0 +1,105 @@ +// +// © Copyright Henrik Ravn 2004 +// +// Use, modification and distribution are subject to the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace DotZLib +{ + + /// + /// Implements a data decompressor, using the inflate algorithm in the ZLib dll + /// + public class Inflater : CodecBase + { + #region Dll imports + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Ansi)] + private static extern int inflateInit_(ref ZStream sz, string vs, int size); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int inflate(ref ZStream sz, int flush); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int inflateReset(ref ZStream sz); + + [DllImport("ZLIB1.dll", CallingConvention=CallingConvention.Cdecl)] + private static extern int inflateEnd(ref ZStream sz); + #endregion + + /// + /// Constructs an new instance of the Inflater + /// + public Inflater() : base() + { + int retval = inflateInit_(ref _ztream, Info.Version, Marshal.SizeOf(_ztream)); + if (retval != 0) + throw new ZLibException(retval, "Could not initialize inflater"); + + resetOutput(); + } + + + /// + /// Adds more data to the codec to be processed. + /// + /// Byte array containing the data to be added to the codec + /// The index of the first byte to add from data + /// The number of bytes to add + /// Adding data may, or may not, raise the DataAvailable event + public override void Add(byte[] data, int offset, int count) + { + if (data == null) throw new ArgumentNullException(); + if (offset < 0 || count < 0) throw new ArgumentOutOfRangeException(); + if ((offset+count) > data.Length) throw new ArgumentException(); + + int total = count; + int inputIndex = offset; + int err = 0; + + while (err >= 0 && inputIndex < total) + { + copyInput(data, inputIndex, Math.Min(total - inputIndex, kBufferSize)); + err = inflate(ref _ztream, (int)FlushTypes.None); + if (err == 0) + while (_ztream.avail_out == 0) + { + OnDataAvailable(); + err = inflate(ref _ztream, (int)FlushTypes.None); + } + + inputIndex += (int)_ztream.total_in; + } + setChecksum( _ztream.adler ); + } + + + /// + /// Finishes up any pending data that needs to be processed and handled. + /// + public override void Finish() + { + int err; + do + { + err = inflate(ref _ztream, (int)FlushTypes.Finish); + OnDataAvailable(); + } + while (err == 0); + setChecksum( _ztream.adler ); + inflateReset(ref _ztream); + resetOutput(); + } + + /// + /// Closes the internal zlib inflate stream + /// + protected override void CleanUp() { inflateEnd(ref _ztream); } + + + } +} diff --git a/compat/zlib/contrib/dotzlib/DotZLib/UnitTests.cs b/compat/zlib/contrib/dotzlib/DotZLib/UnitTests.cs new file mode 100644 index 0000000..4625f50 --- /dev/null +++ b/compat/zlib/contrib/dotzlib/DotZLib/UnitTests.cs @@ -0,0 +1,274 @@ +// +// © Copyright Henrik Ravn 2004 +// +// Use, modification and distribution are subject to the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) +// + +using System; +using System.Collections; +using System.IO; + +// uncomment the define below to include unit tests +//#define nunit +#if nunit +using NUnit.Framework; + +// Unit tests for the DotZLib class library +// ---------------------------------------- +// +// Use this with NUnit 2 from https://www.nunit.org +// + +namespace DotZLibTests +{ + using DotZLib; + + // helper methods + internal class Utils + { + public static bool byteArrEqual( byte[] lhs, byte[] rhs ) + { + if (lhs.Length != rhs.Length) + return false; + for (int i = lhs.Length-1; i >= 0; --i) + if (lhs[i] != rhs[i]) + return false; + return true; + } + + } + + + [TestFixture] + public class CircBufferTests + { + #region Circular buffer tests + [Test] + public void SinglePutGet() + { + CircularBuffer buf = new CircularBuffer(10); + Assert.AreEqual( 0, buf.Size ); + Assert.AreEqual( -1, buf.Get() ); + + Assert.IsTrue(buf.Put( 1 )); + Assert.AreEqual( 1, buf.Size ); + Assert.AreEqual( 1, buf.Get() ); + Assert.AreEqual( 0, buf.Size ); + Assert.AreEqual( -1, buf.Get() ); + } + + [Test] + public void BlockPutGet() + { + CircularBuffer buf = new CircularBuffer(10); + byte[] arr = {1,2,3,4,5,6,7,8,9,10}; + Assert.AreEqual( 10, buf.Put(arr,0,10) ); + Assert.AreEqual( 10, buf.Size ); + Assert.IsFalse( buf.Put(11) ); + Assert.AreEqual( 1, buf.Get() ); + Assert.IsTrue( buf.Put(11) ); + + byte[] arr2 = (byte[])arr.Clone(); + Assert.AreEqual( 9, buf.Get(arr2,1,9) ); + Assert.IsTrue( Utils.byteArrEqual(arr,arr2) ); + } + + #endregion + } + + [TestFixture] + public class ChecksumTests + { + #region CRC32 Tests + [Test] + public void CRC32_Null() + { + CRC32Checksum crc32 = new CRC32Checksum(); + Assert.AreEqual( 0, crc32.Value ); + + crc32 = new CRC32Checksum(1); + Assert.AreEqual( 1, crc32.Value ); + + crc32 = new CRC32Checksum(556); + Assert.AreEqual( 556, crc32.Value ); + } + + [Test] + public void CRC32_Data() + { + CRC32Checksum crc32 = new CRC32Checksum(); + byte[] data = { 1,2,3,4,5,6,7 }; + crc32.Update(data); + Assert.AreEqual( 0x70e46888, crc32.Value ); + + crc32 = new CRC32Checksum(); + crc32.Update("penguin"); + Assert.AreEqual( 0x0e5c1a120, crc32.Value ); + + crc32 = new CRC32Checksum(1); + crc32.Update("penguin"); + Assert.AreEqual(0x43b6aa94, crc32.Value); + + } + #endregion + + #region Adler tests + + [Test] + public void Adler_Null() + { + AdlerChecksum adler = new AdlerChecksum(); + Assert.AreEqual(0, adler.Value); + + adler = new AdlerChecksum(1); + Assert.AreEqual( 1, adler.Value ); + + adler = new AdlerChecksum(556); + Assert.AreEqual( 556, adler.Value ); + } + + [Test] + public void Adler_Data() + { + AdlerChecksum adler = new AdlerChecksum(1); + byte[] data = { 1,2,3,4,5,6,7 }; + adler.Update(data); + Assert.AreEqual( 0x5b001d, adler.Value ); + + adler = new AdlerChecksum(); + adler.Update("penguin"); + Assert.AreEqual(0x0bcf02f6, adler.Value ); + + adler = new AdlerChecksum(1); + adler.Update("penguin"); + Assert.AreEqual(0x0bd602f7, adler.Value); + + } + #endregion + } + + [TestFixture] + public class InfoTests + { + #region Info tests + [Test] + public void Info_Version() + { + Info info = new Info(); + Assert.AreEqual("1.3.2", Info.Version); + Assert.AreEqual(32, info.SizeOfUInt); + Assert.AreEqual(32, info.SizeOfULong); + Assert.AreEqual(32, info.SizeOfPointer); + Assert.AreEqual(32, info.SizeOfOffset); + } + #endregion + } + + [TestFixture] + public class DeflateInflateTests + { + #region Deflate tests + [Test] + public void Deflate_Init() + { + using (Deflater def = new Deflater(CompressLevel.Default)) + { + } + } + + private ArrayList compressedData = new ArrayList(); + private uint adler1; + + private ArrayList uncompressedData = new ArrayList(); + private uint adler2; + + public void CDataAvail(byte[] data, int startIndex, int count) + { + for (int i = 0; i < count; ++i) + compressedData.Add(data[i+startIndex]); + } + + [Test] + public void Deflate_Compress() + { + compressedData.Clear(); + + byte[] testData = new byte[35000]; + for (int i = 0; i < testData.Length; ++i) + testData[i] = 5; + + using (Deflater def = new Deflater((CompressLevel)5)) + { + def.DataAvailable += new DataAvailableHandler(CDataAvail); + def.Add(testData); + def.Finish(); + adler1 = def.Checksum; + } + } + #endregion + + #region Inflate tests + [Test] + public void Inflate_Init() + { + using (Inflater inf = new Inflater()) + { + } + } + + private void DDataAvail(byte[] data, int startIndex, int count) + { + for (int i = 0; i < count; ++i) + uncompressedData.Add(data[i+startIndex]); + } + + [Test] + public void Inflate_Expand() + { + uncompressedData.Clear(); + + using (Inflater inf = new Inflater()) + { + inf.DataAvailable += new DataAvailableHandler(DDataAvail); + inf.Add((byte[])compressedData.ToArray(typeof(byte))); + inf.Finish(); + adler2 = inf.Checksum; + } + Assert.AreEqual( adler1, adler2 ); + } + #endregion + } + + [TestFixture] + public class GZipStreamTests + { + #region GZipStream test + [Test] + public void GZipStream_WriteRead() + { + using (GZipStream gzOut = new GZipStream("gzstream.gz", CompressLevel.Best)) + { + BinaryWriter writer = new BinaryWriter(gzOut); + writer.Write("hi there"); + writer.Write(Math.PI); + writer.Write(42); + } + + using (GZipStream gzIn = new GZipStream("gzstream.gz")) + { + BinaryReader reader = new BinaryReader(gzIn); + string s = reader.ReadString(); + Assert.AreEqual("hi there",s); + double d = reader.ReadDouble(); + Assert.AreEqual(Math.PI, d); + int i = reader.ReadInt32(); + Assert.AreEqual(42,i); + } + + } + #endregion + } +} + +#endif diff --git a/compat/zlib/contrib/dotzlib/LICENSE_1_0.txt b/compat/zlib/contrib/dotzlib/LICENSE_1_0.txt new file mode 100644 index 0000000..127a5bc --- /dev/null +++ b/compat/zlib/contrib/dotzlib/LICENSE_1_0.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/compat/zlib/contrib/dotzlib/readme.txt b/compat/zlib/contrib/dotzlib/readme.txt new file mode 100644 index 0000000..97671ba --- /dev/null +++ b/compat/zlib/contrib/dotzlib/readme.txt @@ -0,0 +1,58 @@ +This directory contains a .Net wrapper class library for the ZLib1.dll + +The wrapper includes support for inflating/deflating memory buffers, +.Net streaming wrappers for the gz streams part of zlib, and wrappers +for the checksum parts of zlib. See DotZLib/UnitTests.cs for examples. + +Directory structure: +-------------------- + +LICENSE_1_0.txt - License file. +readme.txt - This file. +DotZLib.chm - Class library documentation +DotZLib.build - NAnt build file +DotZLib.sln - Microsoft Visual Studio 2003 solution file + +DotZLib\*.cs - Source files for the class library + +Unit tests: +----------- +The file DotZLib/UnitTests.cs contains unit tests for use with NUnit 2.1 or higher. +To include unit tests in the build, define nunit before building. + + +Build instructions: +------------------- + +1. Using Visual Studio.Net 2003: + Open DotZLib.sln in VS.Net and build from there. Output file (DotZLib.dll) + will be found ./DotZLib/bin/release or ./DotZLib/bin/debug, depending on + you are building the release or debug version of the library. Check + DotZLib/UnitTests.cs for instructions on how to include unit tests in the + build. + +2. Using NAnt: + Open a command prompt with access to the build environment and run nant + in the same directory as the DotZLib.build file. + You can define 2 properties on the nant command-line to control the build: + debug={true|false} to toggle between release/debug builds (default=true). + nunit={true|false} to include or exclude unit tests (default=true). + Also the target clean will remove binaries. + Output file (DotZLib.dll) will be found in either ./DotZLib/bin/release + or ./DotZLib/bin/debug, depending on whether you are building the release + or debug version of the library. + + Examples: + nant -D:debug=false -D:nunit=false + will build a release mode version of the library without unit tests. + nant + will build a debug version of the library with unit tests + nant clean + will remove all previously built files. + + +--------------------------------- +Copyright (c) Henrik Ravn 2004 + +Use, modification and distribution are subject to the Boost Software License, Version 1.0. +(See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) diff --git a/compat/zlib/deflate.c b/compat/zlib/deflate.c new file mode 100644 index 0000000..d7d2c7c --- /dev/null +++ b/compat/zlib/deflate.c @@ -0,0 +1,2185 @@ +/* deflate.c -- compress data using the deflation algorithm + * Copyright (C) 1995-2026 Jean-loup Gailly and Mark Adler + * For conditions of distribution and use, see copyright notice in zlib.h + */ + +/* + * ALGORITHM + * + * The "deflation" process depends on being able to identify portions + * of the input text which are identical to earlier input (within a + * sliding window trailing behind the input currently being processed). + * + * The most straightforward technique turns out to be the fastest for + * most input files: try all possible matches and select the longest. + * The key feature of this algorithm is that insertions into the string + * dictionary are very simple and thus fast, and deletions are avoided + * completely. Insertions are performed at each input character, whereas + * string matches are performed only when the previous match ends. So it + * is preferable to spend more time in matches to allow very fast string + * insertions and avoid deletions. The matching algorithm for small + * strings is inspired from that of Rabin & Karp. A brute force approach + * is used to find longer strings when a small match has been found. + * A similar algorithm is used in comic (by Jan-Mark Wams) and freeze + * (by Leonid Broukhis). + * A previous version of this file used a more sophisticated algorithm + * (by Fiala and Greene) which is guaranteed to run in linear amortized + * time, but has a larger average cost, uses more memory and is patented. + * However the F&G algorithm may be faster for some highly redundant + * files if the parameter max_chain_length (described below) is too large. + * + * ACKNOWLEDGEMENTS + * + * The idea of lazy evaluation of matches is due to Jan-Mark Wams, and + * I found it in 'freeze' written by Leonid Broukhis. + * Thanks to many people for bug reports and testing. + * + * REFERENCES + * + * Deutsch, L.P.,"DEFLATE Compressed Data Format Specification". + * Available at https://datatracker.ietf.org/doc/html/rfc1951 + * + * A description of the Rabin and Karp algorithm is given in the book + * "Algorithms" by R. Sedgewick, Addison-Wesley, p252. + * + * Fiala,E.R., and Greene,D.H. + * Data Compression with Finite Windows, Comm.ACM, 32,4 (1989) 490-595 + * + */ + +/* @(#) $Id$ */ + +#include "deflate.h" + +const char deflate_copyright[] = + " deflate 1.3.2 Copyright 1995-2026 Jean-loup Gailly and Mark Adler "; +/* + If you use the zlib library in a product, an acknowledgment is welcome + in the documentation of your product. If for some reason you cannot + include such an acknowledgment, I would appreciate that you keep this + copyright string in the executable of your product. + */ + +typedef enum { + need_more, /* block not completed, need more input or more output */ + block_done, /* block flush performed */ + finish_started, /* finish started, need only more output at next deflate */ + finish_done /* finish done, accept no more input or output */ +} block_state; + +typedef block_state (*compress_func)(deflate_state *s, int flush); +/* Compression function. Returns the block state after the call. */ + +local block_state deflate_stored(deflate_state *s, int flush); +local block_state deflate_fast(deflate_state *s, int flush); +#ifndef FASTEST +local block_state deflate_slow(deflate_state *s, int flush); +#endif +local block_state deflate_rle(deflate_state *s, int flush); +local block_state deflate_huff(deflate_state *s, int flush); + +/* =========================================================================== + * Local data + */ + +#define NIL 0 +/* Tail of hash chains */ + +#ifndef TOO_FAR +# define TOO_FAR 4096 +#endif +/* Matches of length 3 are discarded if their distance exceeds TOO_FAR */ + +/* Values for max_lazy_match, good_match and max_chain_length, depending on + * the desired pack level (0..9). The values given below have been tuned to + * exclude worst case performance for pathological files. Better values may be + * found for specific files. + */ +typedef struct config_s { + ush good_length; /* reduce lazy search above this match length */ + ush max_lazy; /* do not perform lazy search above this match length */ + ush nice_length; /* quit search above this match length */ + ush max_chain; + compress_func func; +} config; + +#ifdef FASTEST +local const config configuration_table[2] = { +/* good lazy nice chain */ +/* 0 */ {0, 0, 0, 0, deflate_stored}, /* store only */ +/* 1 */ {4, 4, 8, 4, deflate_fast}}; /* max speed, no lazy matches */ +#else +local const config configuration_table[10] = { +/* good lazy nice chain */ +/* 0 */ {0, 0, 0, 0, deflate_stored}, /* store only */ +/* 1 */ {4, 4, 8, 4, deflate_fast}, /* max speed, no lazy matches */ +/* 2 */ {4, 5, 16, 8, deflate_fast}, +/* 3 */ {4, 6, 32, 32, deflate_fast}, + +/* 4 */ {4, 4, 16, 16, deflate_slow}, /* lazy matches */ +/* 5 */ {8, 16, 32, 32, deflate_slow}, +/* 6 */ {8, 16, 128, 128, deflate_slow}, +/* 7 */ {8, 32, 128, 256, deflate_slow}, +/* 8 */ {32, 128, 258, 1024, deflate_slow}, +/* 9 */ {32, 258, 258, 4096, deflate_slow}}; /* max compression */ +#endif + +/* Note: the deflate() code requires max_lazy >= MIN_MATCH and max_chain >= 4 + * For deflate_fast() (levels <= 3) good is ignored and lazy has a different + * meaning. + */ + +/* rank Z_BLOCK between Z_NO_FLUSH and Z_PARTIAL_FLUSH */ +#define RANK(f) (((f) * 2) - ((f) > 4 ? 9 : 0)) + +/* =========================================================================== + * Update a hash value with the given input byte + * IN assertion: all calls to UPDATE_HASH are made with consecutive input + * characters, so that a running hash key can be computed from the previous + * key instead of complete recalculation each time. + */ +#define UPDATE_HASH(s,h,c) (h = (((h) << s->hash_shift) ^ (c)) & s->hash_mask) + + +/* =========================================================================== + * Insert string str in the dictionary and set match_head to the previous head + * of the hash chain (the most recent string with same hash key). Return + * the previous length of the hash chain. + * If this file is compiled with -DFASTEST, the compression level is forced + * to 1, and no hash chains are maintained. + * IN assertion: all calls to INSERT_STRING are made with consecutive input + * characters and the first MIN_MATCH bytes of str are valid (except for + * the last MIN_MATCH-1 bytes of the input file). + */ +#ifdef FASTEST +#define INSERT_STRING(s, str, match_head) \ + (UPDATE_HASH(s, s->ins_h, s->window[(str) + (MIN_MATCH-1)]), \ + match_head = s->head[s->ins_h], \ + s->head[s->ins_h] = (Pos)(str)) +#else +#define INSERT_STRING(s, str, match_head) \ + (UPDATE_HASH(s, s->ins_h, s->window[(str) + (MIN_MATCH-1)]), \ + match_head = s->prev[(str) & s->w_mask] = s->head[s->ins_h], \ + s->head[s->ins_h] = (Pos)(str)) +#endif + +/* =========================================================================== + * Initialize the hash table (avoiding 64K overflow for 16 bit systems). + * prev[] will be initialized on the fly. + */ +#define CLEAR_HASH(s) \ + do { \ + s->head[s->hash_size - 1] = NIL; \ + zmemzero(s->head, (unsigned)(s->hash_size - 1)*sizeof(*s->head)); \ + s->slid = 0; \ + } while (0) + +/* =========================================================================== + * Slide the hash table when sliding the window down (could be avoided with 32 + * bit values at the expense of memory usage). We slide even when level == 0 to + * keep the hash table consistent if we switch back to level > 0 later. + */ +#if defined(__has_feature) +# if __has_feature(memory_sanitizer) + __attribute__((no_sanitize("memory"))) +# endif +#endif +local void slide_hash(deflate_state *s) { + unsigned n, m; + Posf *p; + uInt wsize = s->w_size; + + n = s->hash_size; + p = &s->head[n]; + do { + m = *--p; + *p = (Pos)(m >= wsize ? m - wsize : NIL); + } while (--n); +#ifndef FASTEST + n = wsize; + p = &s->prev[n]; + do { + m = *--p; + *p = (Pos)(m >= wsize ? m - wsize : NIL); + /* If n is not on any hash chain, prev[n] is garbage but + * its value will never be used. + */ + } while (--n); +#endif + s->slid = 1; +} + +/* =========================================================================== + * Read a new buffer from the current input stream, update the adler32 + * and total number of bytes read. All deflate() input goes through + * this function so some applications may wish to modify it to avoid + * allocating a large strm->next_in buffer and copying from it. + * (See also flush_pending()). + */ +local unsigned read_buf(z_streamp strm, Bytef *buf, unsigned size) { + unsigned len = strm->avail_in; + + if (len > size) len = size; + if (len == 0) return 0; + + strm->avail_in -= len; + + zmemcpy(buf, strm->next_in, len); + if (strm->state->wrap == 1) { + strm->adler = adler32(strm->adler, buf, len); + } +#ifdef GZIP + else if (strm->state->wrap == 2) { + strm->adler = crc32(strm->adler, buf, len); + } +#endif + strm->next_in += len; + strm->total_in += len; + + return len; +} + +/* =========================================================================== + * Fill the window when the lookahead becomes insufficient. + * Updates strstart and lookahead. + * + * IN assertion: lookahead < MIN_LOOKAHEAD + * OUT assertions: strstart <= window_size-MIN_LOOKAHEAD + * At least one byte has been read, or avail_in == 0; reads are + * performed for at least two bytes (required for the zip translate_eol + * option -- not supported here). + */ +local void fill_window(deflate_state *s) { + unsigned n; + unsigned more; /* Amount of free space at the end of the window. */ + uInt wsize = s->w_size; + + Assert(s->lookahead < MIN_LOOKAHEAD, "already enough lookahead"); + + do { + more = (unsigned)(s->window_size -(ulg)s->lookahead -(ulg)s->strstart); + + /* Deal with !@#$% 64K limit: */ +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4127) +#endif + if (sizeof(int) <= 2) { +#ifdef _MSC_VER +#pragma warning(pop) +#endif + if (more == 0 && s->strstart == 0 && s->lookahead == 0) { + more = wsize; + + } else if (more == (unsigned)(-1)) { + /* Very unlikely, but possible on 16 bit machine if + * strstart == 0 && lookahead == 1 (input done a byte at time) + */ + more--; + } + } + + /* If the window is almost full and there is insufficient lookahead, + * move the upper half to the lower one to make room in the upper half. + */ + if (s->strstart >= wsize + MAX_DIST(s)) { + + zmemcpy(s->window, s->window + wsize, (unsigned)wsize - more); + s->match_start -= wsize; + s->strstart -= wsize; /* we now have strstart >= MAX_DIST */ + s->block_start -= (long) wsize; + if (s->insert > s->strstart) + s->insert = s->strstart; + slide_hash(s); + more += wsize; + } + if (s->strm->avail_in == 0) break; + + /* If there was no sliding: + * strstart <= WSIZE+MAX_DIST-1 && lookahead <= MIN_LOOKAHEAD - 1 && + * more == window_size - lookahead - strstart + * => more >= window_size - (MIN_LOOKAHEAD-1 + WSIZE + MAX_DIST-1) + * => more >= window_size - 2*WSIZE + 2 + * In the BIG_MEM or MMAP case (not yet supported), + * window_size == input_size + MIN_LOOKAHEAD && + * strstart + s->lookahead <= input_size => more >= MIN_LOOKAHEAD. + * Otherwise, window_size == 2*WSIZE so more >= 2. + * If there was sliding, more >= WSIZE. So in all cases, more >= 2. + */ + Assert(more >= 2, "more < 2"); + + n = read_buf(s->strm, s->window + s->strstart + s->lookahead, more); + s->lookahead += n; + + /* Initialize the hash value now that we have some input: */ + if (s->lookahead + s->insert >= MIN_MATCH) { + uInt str = s->strstart - s->insert; + s->ins_h = s->window[str]; + UPDATE_HASH(s, s->ins_h, s->window[str + 1]); +#if MIN_MATCH != 3 + Call UPDATE_HASH() MIN_MATCH-3 more times +#endif + while (s->insert) { + UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); +#ifndef FASTEST + s->prev[str & s->w_mask] = s->head[s->ins_h]; +#endif + s->head[s->ins_h] = (Pos)str; + str++; + s->insert--; + if (s->lookahead + s->insert < MIN_MATCH) + break; + } + } + /* If the whole input has less than MIN_MATCH bytes, ins_h is garbage, + * but this is not important since only literal bytes will be emitted. + */ + + } while (s->lookahead < MIN_LOOKAHEAD && s->strm->avail_in != 0); + + /* If the WIN_INIT bytes after the end of the current data have never been + * written, then zero those bytes in order to avoid memory check reports of + * the use of uninitialized (or uninitialised as Julian writes) bytes by + * the longest match routines. Update the high water mark for the next + * time through here. WIN_INIT is set to MAX_MATCH since the longest match + * routines allow scanning to strstart + MAX_MATCH, ignoring lookahead. + */ + if (s->high_water < s->window_size) { + ulg curr = s->strstart + (ulg)(s->lookahead); + ulg init; + + if (s->high_water < curr) { + /* Previous high water mark below current data -- zero WIN_INIT + * bytes or up to end of window, whichever is less. + */ + init = s->window_size - curr; + if (init > WIN_INIT) + init = WIN_INIT; + zmemzero(s->window + curr, (unsigned)init); + s->high_water = curr + init; + } + else if (s->high_water < (ulg)curr + WIN_INIT) { + /* High water mark at or above current data, but below current data + * plus WIN_INIT -- zero out to current data plus WIN_INIT, or up + * to end of window, whichever is less. + */ + init = (ulg)curr + WIN_INIT - s->high_water; + if (init > s->window_size - s->high_water) + init = s->window_size - s->high_water; + zmemzero(s->window + s->high_water, (unsigned)init); + s->high_water += init; + } + } + + Assert((ulg)s->strstart <= s->window_size - MIN_LOOKAHEAD, + "not enough room for search"); +} + +/* ========================================================================= */ +int ZEXPORT deflateInit_(z_streamp strm, int level, const char *version, + int stream_size) { + return deflateInit2_(strm, level, Z_DEFLATED, MAX_WBITS, DEF_MEM_LEVEL, + Z_DEFAULT_STRATEGY, version, stream_size); + /* To do: ignore strm->next_in if we use it as window */ +} + +/* ========================================================================= */ +int ZEXPORT deflateInit2_(z_streamp strm, int level, int method, + int windowBits, int memLevel, int strategy, + const char *version, int stream_size) { + deflate_state *s; + int wrap = 1; + static const char my_version[] = ZLIB_VERSION; + + if (version == Z_NULL || version[0] != my_version[0] || + stream_size != sizeof(z_stream)) { + return Z_VERSION_ERROR; + } + if (strm == Z_NULL) return Z_STREAM_ERROR; + + strm->msg = Z_NULL; + if (strm->zalloc == (alloc_func)0) { +#ifdef Z_SOLO + return Z_STREAM_ERROR; +#else + strm->zalloc = zcalloc; + strm->opaque = (voidpf)0; +#endif + } + if (strm->zfree == (free_func)0) +#ifdef Z_SOLO + return Z_STREAM_ERROR; +#else + strm->zfree = zcfree; +#endif + +#ifdef FASTEST + if (level != 0) level = 1; +#else + if (level == Z_DEFAULT_COMPRESSION) level = 6; +#endif + + if (windowBits < 0) { /* suppress zlib wrapper */ + wrap = 0; + if (windowBits < -15) + return Z_STREAM_ERROR; + windowBits = -windowBits; + } +#ifdef GZIP + else if (windowBits > 15) { + wrap = 2; /* write gzip wrapper instead */ + windowBits -= 16; + } +#endif + if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method != Z_DEFLATED || + windowBits < 8 || windowBits > 15 || level < 0 || level > 9 || + strategy < 0 || strategy > Z_FIXED || (windowBits == 8 && wrap != 1)) { + return Z_STREAM_ERROR; + } + if (windowBits == 8) windowBits = 9; /* until 256-byte window bug fixed */ + s = (deflate_state *) ZALLOC(strm, 1, sizeof(deflate_state)); + if (s == Z_NULL) return Z_MEM_ERROR; + zmemzero(s, sizeof(deflate_state)); + strm->state = (struct internal_state FAR *)s; + s->strm = strm; + s->status = INIT_STATE; /* to pass state test in deflateReset() */ + + s->wrap = wrap; + s->gzhead = Z_NULL; + s->w_bits = (uInt)windowBits; + s->w_size = 1 << s->w_bits; + s->w_mask = s->w_size - 1; + + s->hash_bits = (uInt)memLevel + 7; + s->hash_size = 1 << s->hash_bits; + s->hash_mask = s->hash_size - 1; + s->hash_shift = ((s->hash_bits + MIN_MATCH-1) / MIN_MATCH); + + s->window = (Bytef *) ZALLOC(strm, s->w_size, 2*sizeof(Byte)); + s->prev = (Posf *) ZALLOC(strm, s->w_size, sizeof(Pos)); + s->head = (Posf *) ZALLOC(strm, s->hash_size, sizeof(Pos)); + + s->high_water = 0; /* nothing written to s->window yet */ + + s->lit_bufsize = 1 << (memLevel + 6); /* 16K elements by default */ + + /* We overlay pending_buf and sym_buf. This works since the average size + * for length/distance pairs over any compressed block is assured to be 31 + * bits or less. + * + * Analysis: The longest fixed codes are a length code of 8 bits plus 5 + * extra bits, for lengths 131 to 257. The longest fixed distance codes are + * 5 bits plus 13 extra bits, for distances 16385 to 32768. The longest + * possible fixed-codes length/distance pair is then 31 bits total. + * + * sym_buf starts one-fourth of the way into pending_buf. So there are + * three bytes in sym_buf for every four bytes in pending_buf. Each symbol + * in sym_buf is three bytes -- two for the distance and one for the + * literal/length. As each symbol is consumed, the pointer to the next + * sym_buf value to read moves forward three bytes. From that symbol, up to + * 31 bits are written to pending_buf. The closest the written pending_buf + * bits gets to the next sym_buf symbol to read is just before the last + * code is written. At that time, 31*(n - 2) bits have been written, just + * after 24*(n - 2) bits have been consumed from sym_buf. sym_buf starts at + * 8*n bits into pending_buf. (Note that the symbol buffer fills when n - 1 + * symbols are written.) The closest the writing gets to what is unread is + * then n + 14 bits. Here n is lit_bufsize, which is 16384 by default, and + * can range from 128 to 32768. + * + * Therefore, at a minimum, there are 142 bits of space between what is + * written and what is read in the overlain buffers, so the symbols cannot + * be overwritten by the compressed data. That space is actually 139 bits, + * due to the three-bit fixed-code block header. + * + * That covers the case where either Z_FIXED is specified, forcing fixed + * codes, or when the use of fixed codes is chosen, because that choice + * results in a smaller compressed block than dynamic codes. That latter + * condition then assures that the above analysis also covers all dynamic + * blocks. A dynamic-code block will only be chosen to be emitted if it has + * fewer bits than a fixed-code block would for the same set of symbols. + * Therefore its average symbol length is assured to be less than 31. So + * the compressed data for a dynamic block also cannot overwrite the + * symbols from which it is being constructed. + */ + + s->pending_buf = (uchf *) ZALLOC(strm, s->lit_bufsize, LIT_BUFS); + s->pending_buf_size = (ulg)s->lit_bufsize * 4; + + if (s->window == Z_NULL || s->prev == Z_NULL || s->head == Z_NULL || + s->pending_buf == Z_NULL) { + s->status = FINISH_STATE; + strm->msg = ERR_MSG(Z_MEM_ERROR); + deflateEnd (strm); + return Z_MEM_ERROR; + } +#ifdef LIT_MEM + s->d_buf = (ushf *)(s->pending_buf + (s->lit_bufsize << 1)); + s->l_buf = s->pending_buf + (s->lit_bufsize << 2); + s->sym_end = s->lit_bufsize - 1; +#else + s->sym_buf = s->pending_buf + s->lit_bufsize; + s->sym_end = (s->lit_bufsize - 1) * 3; +#endif + /* We avoid equality with lit_bufsize*3 because of wraparound at 64K + * on 16 bit machines and because stored blocks are restricted to + * 64K-1 bytes. + */ + + s->level = level; + s->strategy = strategy; + s->method = (Byte)method; + + return deflateReset(strm); +} + +/* ========================================================================= + * Check for a valid deflate stream state. Return 0 if ok, 1 if not. + */ +local int deflateStateCheck(z_streamp strm) { + deflate_state *s; + if (strm == Z_NULL || + strm->zalloc == (alloc_func)0 || strm->zfree == (free_func)0) + return 1; + s = strm->state; + if (s == Z_NULL || s->strm != strm || (s->status != INIT_STATE && +#ifdef GZIP + s->status != GZIP_STATE && +#endif + s->status != EXTRA_STATE && + s->status != NAME_STATE && + s->status != COMMENT_STATE && + s->status != HCRC_STATE && + s->status != BUSY_STATE && + s->status != FINISH_STATE)) + return 1; + return 0; +} + +/* ========================================================================= */ +int ZEXPORT deflateSetDictionary(z_streamp strm, const Bytef *dictionary, + uInt dictLength) { + deflate_state *s; + uInt str, n; + int wrap; + unsigned avail; + z_const unsigned char *next; + + if (deflateStateCheck(strm) || dictionary == Z_NULL) + return Z_STREAM_ERROR; + s = strm->state; + wrap = s->wrap; + if (wrap == 2 || (wrap == 1 && s->status != INIT_STATE) || s->lookahead) + return Z_STREAM_ERROR; + + /* when using zlib wrappers, compute Adler-32 for provided dictionary */ + if (wrap == 1) + strm->adler = adler32(strm->adler, dictionary, dictLength); + s->wrap = 0; /* avoid computing Adler-32 in read_buf */ + + /* if dictionary would fill window, just replace the history */ + if (dictLength >= s->w_size) { + if (wrap == 0) { /* already empty otherwise */ + CLEAR_HASH(s); + s->strstart = 0; + s->block_start = 0L; + s->insert = 0; + } + dictionary += dictLength - s->w_size; /* use the tail */ + dictLength = s->w_size; + } + + /* insert dictionary into window and hash */ + avail = strm->avail_in; + next = strm->next_in; + strm->avail_in = dictLength; + strm->next_in = (z_const Bytef *)dictionary; + fill_window(s); + while (s->lookahead >= MIN_MATCH) { + str = s->strstart; + n = s->lookahead - (MIN_MATCH-1); + do { + UPDATE_HASH(s, s->ins_h, s->window[str + MIN_MATCH-1]); +#ifndef FASTEST + s->prev[str & s->w_mask] = s->head[s->ins_h]; +#endif + s->head[s->ins_h] = (Pos)str; + str++; + } while (--n); + s->strstart = str; + s->lookahead = MIN_MATCH-1; + fill_window(s); + } + s->strstart += s->lookahead; + s->block_start = (long)s->strstart; + s->insert = s->lookahead; + s->lookahead = 0; + s->match_length = s->prev_length = MIN_MATCH-1; + s->match_available = 0; + strm->next_in = next; + strm->avail_in = avail; + s->wrap = wrap; + return Z_OK; +} + +/* ========================================================================= */ +int ZEXPORT deflateGetDictionary(z_streamp strm, Bytef *dictionary, + uInt *dictLength) { + deflate_state *s; + uInt len; + + if (deflateStateCheck(strm)) + return Z_STREAM_ERROR; + s = strm->state; + len = s->strstart + s->lookahead; + if (len > s->w_size) + len = s->w_size; + if (dictionary != Z_NULL && len) + zmemcpy(dictionary, s->window + s->strstart + s->lookahead - len, len); + if (dictLength != Z_NULL) + *dictLength = len; + return Z_OK; +} + +/* ========================================================================= */ +int ZEXPORT deflateResetKeep(z_streamp strm) { + deflate_state *s; + + if (deflateStateCheck(strm)) { + return Z_STREAM_ERROR; + } + + strm->total_in = strm->total_out = 0; + strm->msg = Z_NULL; /* use zfree if we ever allocate msg dynamically */ + strm->data_type = Z_UNKNOWN; + + s = (deflate_state *)strm->state; + s->pending = 0; + s->pending_out = s->pending_buf; + + if (s->wrap < 0) { + s->wrap = -s->wrap; /* was made negative by deflate(..., Z_FINISH); */ + } + s->status = +#ifdef GZIP + s->wrap == 2 ? GZIP_STATE : +#endif + INIT_STATE; + strm->adler = +#ifdef GZIP + s->wrap == 2 ? crc32(0L, Z_NULL, 0) : +#endif + adler32(0L, Z_NULL, 0); + s->last_flush = -2; + + _tr_init(s); + + return Z_OK; +} + +/* =========================================================================== + * Initialize the "longest match" routines for a new zlib stream + */ +local void lm_init(deflate_state *s) { + s->window_size = (ulg)2L*s->w_size; + + CLEAR_HASH(s); + + /* Set the default configuration parameters: + */ + s->max_lazy_match = configuration_table[s->level].max_lazy; + s->good_match = configuration_table[s->level].good_length; + s->nice_match = configuration_table[s->level].nice_length; + s->max_chain_length = configuration_table[s->level].max_chain; + + s->strstart = 0; + s->block_start = 0L; + s->lookahead = 0; + s->insert = 0; + s->match_length = s->prev_length = MIN_MATCH-1; + s->match_available = 0; + s->ins_h = 0; +} + +/* ========================================================================= */ +int ZEXPORT deflateReset(z_streamp strm) { + int ret; + + ret = deflateResetKeep(strm); + if (ret == Z_OK) + lm_init(strm->state); + return ret; +} + +/* ========================================================================= */ +int ZEXPORT deflateSetHeader(z_streamp strm, gz_headerp head) { + if (deflateStateCheck(strm) || strm->state->wrap != 2) + return Z_STREAM_ERROR; + strm->state->gzhead = head; + return Z_OK; +} + +/* ========================================================================= */ +int ZEXPORT deflatePending(z_streamp strm, unsigned *pending, int *bits) { + if (deflateStateCheck(strm)) return Z_STREAM_ERROR; + if (bits != Z_NULL) + *bits = strm->state->bi_valid; + if (pending != Z_NULL) { + *pending = (unsigned)strm->state->pending; + if (*pending != strm->state->pending) { + *pending = (unsigned)-1; + return Z_BUF_ERROR; + } + } + return Z_OK; +} + +/* ========================================================================= */ +int ZEXPORT deflateUsed(z_streamp strm, int *bits) { + if (deflateStateCheck(strm)) return Z_STREAM_ERROR; + if (bits != Z_NULL) + *bits = strm->state->bi_used; + return Z_OK; +} + +/* ========================================================================= */ +int ZEXPORT deflatePrime(z_streamp strm, int bits, int value) { + deflate_state *s; + int put; + + if (deflateStateCheck(strm)) return Z_STREAM_ERROR; + s = strm->state; +#ifdef LIT_MEM + if (bits < 0 || bits > 16 || + (uchf *)s->d_buf < s->pending_out + ((Buf_size + 7) >> 3)) + return Z_BUF_ERROR; +#else + if (bits < 0 || bits > 16 || + s->sym_buf < s->pending_out + ((Buf_size + 7) >> 3)) + return Z_BUF_ERROR; +#endif + do { + put = Buf_size - s->bi_valid; + if (put > bits) + put = bits; + s->bi_buf |= (ush)((value & ((1 << put) - 1)) << s->bi_valid); + s->bi_valid += put; + _tr_flush_bits(s); + value >>= put; + bits -= put; + } while (bits); + return Z_OK; +} + +/* ========================================================================= */ +int ZEXPORT deflateParams(z_streamp strm, int level, int strategy) { + deflate_state *s; + compress_func func; + + if (deflateStateCheck(strm)) return Z_STREAM_ERROR; + s = strm->state; + +#ifdef FASTEST + if (level != 0) level = 1; +#else + if (level == Z_DEFAULT_COMPRESSION) level = 6; +#endif + if (level < 0 || level > 9 || strategy < 0 || strategy > Z_FIXED) { + return Z_STREAM_ERROR; + } + func = configuration_table[s->level].func; + + if ((strategy != s->strategy || func != configuration_table[level].func) && + s->last_flush != -2) { + /* Flush the last buffer: */ + int err = deflate(strm, Z_BLOCK); + if (err == Z_STREAM_ERROR) + return err; + if (strm->avail_in || (s->strstart - s->block_start) + s->lookahead) + return Z_BUF_ERROR; + } + if (s->level != level) { + if (s->level == 0 && s->matches != 0) { + if (s->matches == 1) + slide_hash(s); + else + CLEAR_HASH(s); + s->matches = 0; + } + s->level = level; + s->max_lazy_match = configuration_table[level].max_lazy; + s->good_match = configuration_table[level].good_length; + s->nice_match = configuration_table[level].nice_length; + s->max_chain_length = configuration_table[level].max_chain; + } + s->strategy = strategy; + return Z_OK; +} + +/* ========================================================================= */ +int ZEXPORT deflateTune(z_streamp strm, int good_length, int max_lazy, + int nice_length, int max_chain) { + deflate_state *s; + + if (deflateStateCheck(strm)) return Z_STREAM_ERROR; + s = strm->state; + s->good_match = (uInt)good_length; + s->max_lazy_match = (uInt)max_lazy; + s->nice_match = nice_length; + s->max_chain_length = (uInt)max_chain; + return Z_OK; +} + +/* ========================================================================= + * For the default windowBits of 15 and memLevel of 8, this function returns a + * close to exact, as well as small, upper bound on the compressed size. This + * is an expansion of ~0.03%, plus a small constant. + * + * For any setting other than those defaults for windowBits and memLevel, one + * of two worst case bounds is returned. This is at most an expansion of ~4% or + * ~13%, plus a small constant. + * + * Both the 0.03% and 4% derive from the overhead of stored blocks. The first + * one is for stored blocks of 16383 bytes (memLevel == 8), whereas the second + * is for stored blocks of 127 bytes (the worst case memLevel == 1). The + * expansion results from five bytes of header for each stored block. + * + * The larger expansion of 13% results from a window size less than or equal to + * the symbols buffer size (windowBits <= memLevel + 7). In that case some of + * the data being compressed may have slid out of the sliding window, impeding + * a stored block from being emitted. Then the only choice is a fixed or + * dynamic block, where a fixed block limits the maximum expansion to 9 bits + * per 8-bit byte, plus 10 bits for every block. The smallest block size for + * which this can occur is 255 (memLevel == 2). + * + * Shifts are used to approximate divisions, for speed. + */ +z_size_t ZEXPORT deflateBound_z(z_streamp strm, z_size_t sourceLen) { + deflate_state *s; + z_size_t fixedlen, storelen, wraplen, bound; + + /* upper bound for fixed blocks with 9-bit literals and length 255 + (memLevel == 2, which is the lowest that may not use stored blocks) -- + ~13% overhead plus a small constant */ + fixedlen = sourceLen + (sourceLen >> 3) + (sourceLen >> 8) + + (sourceLen >> 9) + 4; + if (fixedlen < sourceLen) + fixedlen = (z_size_t)-1; + + /* upper bound for stored blocks with length 127 (memLevel == 1) -- + ~4% overhead plus a small constant */ + storelen = sourceLen + (sourceLen >> 5) + (sourceLen >> 7) + + (sourceLen >> 11) + 7; + if (storelen < sourceLen) + storelen = (z_size_t)-1; + + /* if can't get parameters, return larger bound plus a wrapper */ + if (deflateStateCheck(strm)) { + bound = fixedlen > storelen ? fixedlen : storelen; + return bound + 18 < bound ? (z_size_t)-1 : bound + 18; + } + + /* compute wrapper length */ + s = strm->state; + switch (s->wrap < 0 ? -s->wrap : s->wrap) { + case 0: /* raw deflate */ + wraplen = 0; + break; + case 1: /* zlib wrapper */ + wraplen = 6 + (s->strstart ? 4 : 0); + break; +#ifdef GZIP + case 2: /* gzip wrapper */ + wraplen = 18; + if (s->gzhead != Z_NULL) { /* user-supplied gzip header */ + Bytef *str; + if (s->gzhead->extra != Z_NULL) + wraplen += 2 + s->gzhead->extra_len; + str = s->gzhead->name; + if (str != Z_NULL) + do { + wraplen++; + } while (*str++); + str = s->gzhead->comment; + if (str != Z_NULL) + do { + wraplen++; + } while (*str++); + if (s->gzhead->hcrc) + wraplen += 2; + } + break; +#endif + default: /* for compiler happiness */ + wraplen = 18; + } + + /* if not default parameters, return one of the conservative bounds */ + if (s->w_bits != 15 || s->hash_bits != 8 + 7) { + bound = s->w_bits <= s->hash_bits && s->level ? fixedlen : + storelen; + return bound + wraplen < bound ? (z_size_t)-1 : bound + wraplen; + } + + /* default settings: return tight bound for that case -- ~0.03% overhead + plus a small constant */ + bound = sourceLen + (sourceLen >> 12) + (sourceLen >> 14) + + (sourceLen >> 25) + 13 - 6 + wraplen; + return bound < sourceLen ? (z_size_t)-1 : bound; +} +uLong ZEXPORT deflateBound(z_streamp strm, uLong sourceLen) { + z_size_t bound = deflateBound_z(strm, sourceLen); + return (uLong)bound != bound ? (uLong)-1 : (uLong)bound; +} + +/* ========================================================================= + * Put a short in the pending buffer. The 16-bit value is put in MSB order. + * IN assertion: the stream state is correct and there is enough room in + * pending_buf. + */ +local void putShortMSB(deflate_state *s, uInt b) { + put_byte(s, (Byte)(b >> 8)); + put_byte(s, (Byte)(b & 0xff)); +} + +/* ========================================================================= + * Flush as much pending output as possible. All deflate() output, except for + * some deflate_stored() output, goes through this function so some + * applications may wish to modify it to avoid allocating a large + * strm->next_out buffer and copying into it. (See also read_buf()). + */ +local void flush_pending(z_streamp strm) { + unsigned len; + deflate_state *s = strm->state; + + _tr_flush_bits(s); + len = s->pending > strm->avail_out ? strm->avail_out : + (unsigned)s->pending; + if (len == 0) return; + + zmemcpy(strm->next_out, s->pending_out, len); + strm->next_out += len; + s->pending_out += len; + strm->total_out += len; + strm->avail_out -= len; + s->pending -= len; + if (s->pending == 0) { + s->pending_out = s->pending_buf; + } +} + +/* =========================================================================== + * Update the header CRC with the bytes s->pending_buf[beg..s->pending - 1]. + */ +#define HCRC_UPDATE(beg) \ + do { \ + if (s->gzhead->hcrc && s->pending > (beg)) \ + strm->adler = crc32_z(strm->adler, s->pending_buf + (beg), \ + s->pending - (beg)); \ + } while (0) + +/* ========================================================================= */ +int ZEXPORT deflate(z_streamp strm, int flush) { + int old_flush; /* value of flush param for previous deflate call */ + deflate_state *s; + + if (deflateStateCheck(strm) || flush > Z_BLOCK || flush < 0) { + return Z_STREAM_ERROR; + } + s = strm->state; + + if (strm->next_out == Z_NULL || + (strm->avail_in != 0 && strm->next_in == Z_NULL) || + (s->status == FINISH_STATE && flush != Z_FINISH)) { + ERR_RETURN(strm, Z_STREAM_ERROR); + } + if (strm->avail_out == 0) ERR_RETURN(strm, Z_BUF_ERROR); + + old_flush = s->last_flush; + s->last_flush = flush; + + /* Flush as much pending output as possible */ + if (s->pending != 0) { + flush_pending(strm); + if (strm->avail_out == 0) { + /* Since avail_out is 0, deflate will be called again with + * more output space, but possibly with both pending and + * avail_in equal to zero. There won't be anything to do, + * but this is not an error situation so make sure we + * return OK instead of BUF_ERROR at next call of deflate: + */ + s->last_flush = -1; + return Z_OK; + } + + /* Make sure there is something to do and avoid duplicate consecutive + * flushes. For repeated and useless calls with Z_FINISH, we keep + * returning Z_STREAM_END instead of Z_BUF_ERROR. + */ + } else if (strm->avail_in == 0 && RANK(flush) <= RANK(old_flush) && + flush != Z_FINISH) { + ERR_RETURN(strm, Z_BUF_ERROR); + } + + /* User must not provide more input after the first FINISH: */ + if (s->status == FINISH_STATE && strm->avail_in != 0) { + ERR_RETURN(strm, Z_BUF_ERROR); + } + + /* Write the header */ + if (s->status == INIT_STATE && s->wrap == 0) + s->status = BUSY_STATE; + if (s->status == INIT_STATE) { + /* zlib header */ + uInt header = (Z_DEFLATED + ((s->w_bits - 8) << 4)) << 8; + uInt level_flags; + + if (s->strategy >= Z_HUFFMAN_ONLY || s->level < 2) + level_flags = 0; + else if (s->level < 6) + level_flags = 1; + else if (s->level == 6) + level_flags = 2; + else + level_flags = 3; + header |= (level_flags << 6); + if (s->strstart != 0) header |= PRESET_DICT; + header += 31 - (header % 31); + + putShortMSB(s, header); + + /* Save the adler32 of the preset dictionary: */ + if (s->strstart != 0) { + putShortMSB(s, (uInt)(strm->adler >> 16)); + putShortMSB(s, (uInt)(strm->adler & 0xffff)); + } + strm->adler = adler32(0L, Z_NULL, 0); + s->status = BUSY_STATE; + + /* Compression must start with an empty pending buffer */ + flush_pending(strm); + if (s->pending != 0) { + s->last_flush = -1; + return Z_OK; + } + } +#ifdef GZIP + if (s->status == GZIP_STATE) { + /* gzip header */ + strm->adler = crc32(0L, Z_NULL, 0); + put_byte(s, 31); + put_byte(s, 139); + put_byte(s, 8); + if (s->gzhead == Z_NULL) { + put_byte(s, 0); + put_byte(s, 0); + put_byte(s, 0); + put_byte(s, 0); + put_byte(s, 0); + put_byte(s, s->level == 9 ? 2 : + (s->strategy >= Z_HUFFMAN_ONLY || s->level < 2 ? + 4 : 0)); + put_byte(s, OS_CODE); + s->status = BUSY_STATE; + + /* Compression must start with an empty pending buffer */ + flush_pending(strm); + if (s->pending != 0) { + s->last_flush = -1; + return Z_OK; + } + } + else { + put_byte(s, (s->gzhead->text ? 1 : 0) + + (s->gzhead->hcrc ? 2 : 0) + + (s->gzhead->extra == Z_NULL ? 0 : 4) + + (s->gzhead->name == Z_NULL ? 0 : 8) + + (s->gzhead->comment == Z_NULL ? 0 : 16) + ); + put_byte(s, (Byte)(s->gzhead->time & 0xff)); + put_byte(s, (Byte)((s->gzhead->time >> 8) & 0xff)); + put_byte(s, (Byte)((s->gzhead->time >> 16) & 0xff)); + put_byte(s, (Byte)((s->gzhead->time >> 24) & 0xff)); + put_byte(s, s->level == 9 ? 2 : + (s->strategy >= Z_HUFFMAN_ONLY || s->level < 2 ? + 4 : 0)); + put_byte(s, s->gzhead->os & 0xff); + if (s->gzhead->extra != Z_NULL) { + put_byte(s, s->gzhead->extra_len & 0xff); + put_byte(s, (s->gzhead->extra_len >> 8) & 0xff); + } + if (s->gzhead->hcrc) + strm->adler = crc32_z(strm->adler, s->pending_buf, + s->pending); + s->gzindex = 0; + s->status = EXTRA_STATE; + } + } + if (s->status == EXTRA_STATE) { + if (s->gzhead->extra != Z_NULL) { + ulg beg = s->pending; /* start of bytes to update crc */ + ulg left = (s->gzhead->extra_len & 0xffff) - s->gzindex; + while (s->pending + left > s->pending_buf_size) { + ulg copy = s->pending_buf_size - s->pending; + zmemcpy(s->pending_buf + s->pending, + s->gzhead->extra + s->gzindex, copy); + s->pending = s->pending_buf_size; + HCRC_UPDATE(beg); + s->gzindex += copy; + flush_pending(strm); + if (s->pending != 0) { + s->last_flush = -1; + return Z_OK; + } + beg = 0; + left -= copy; + } + zmemcpy(s->pending_buf + s->pending, + s->gzhead->extra + s->gzindex, left); + s->pending += left; + HCRC_UPDATE(beg); + s->gzindex = 0; + } + s->status = NAME_STATE; + } + if (s->status == NAME_STATE) { + if (s->gzhead->name != Z_NULL) { + ulg beg = s->pending; /* start of bytes to update crc */ + int val; + do { + if (s->pending == s->pending_buf_size) { + HCRC_UPDATE(beg); + flush_pending(strm); + if (s->pending != 0) { + s->last_flush = -1; + return Z_OK; + } + beg = 0; + } + val = s->gzhead->name[s->gzindex++]; + put_byte(s, val); + } while (val != 0); + HCRC_UPDATE(beg); + s->gzindex = 0; + } + s->status = COMMENT_STATE; + } + if (s->status == COMMENT_STATE) { + if (s->gzhead->comment != Z_NULL) { + ulg beg = s->pending; /* start of bytes to update crc */ + int val; + do { + if (s->pending == s->pending_buf_size) { + HCRC_UPDATE(beg); + flush_pending(strm); + if (s->pending != 0) { + s->last_flush = -1; + return Z_OK; + } + beg = 0; + } + val = s->gzhead->comment[s->gzindex++]; + put_byte(s, val); + } while (val != 0); + HCRC_UPDATE(beg); + } + s->status = HCRC_STATE; + } + if (s->status == HCRC_STATE) { + if (s->gzhead->hcrc) { + if (s->pending + 2 > s->pending_buf_size) { + flush_pending(strm); + if (s->pending != 0) { + s->last_flush = -1; + return Z_OK; + } + } + put_byte(s, (Byte)(strm->adler & 0xff)); + put_byte(s, (Byte)((strm->adler >> 8) & 0xff)); + strm->adler = crc32(0L, Z_NULL, 0); + } + s->status = BUSY_STATE; + + /* Compression must start with an empty pending buffer */ + flush_pending(strm); + if (s->pending != 0) { + s->last_flush = -1; + return Z_OK; + } + } +#endif + + /* Start a new block or continue the current one. + */ + if (strm->avail_in != 0 || s->lookahead != 0 || + (flush != Z_NO_FLUSH && s->status != FINISH_STATE)) { + block_state bstate; + + bstate = s->level == 0 ? deflate_stored(s, flush) : + s->strategy == Z_HUFFMAN_ONLY ? deflate_huff(s, flush) : + s->strategy == Z_RLE ? deflate_rle(s, flush) : + (*(configuration_table[s->level].func))(s, flush); + + if (bstate == finish_started || bstate == finish_done) { + s->status = FINISH_STATE; + } + if (bstate == need_more || bstate == finish_started) { + if (strm->avail_out == 0) { + s->last_flush = -1; /* avoid BUF_ERROR next call, see above */ + } + return Z_OK; + /* If flush != Z_NO_FLUSH && avail_out == 0, the next call + * of deflate should use the same flush parameter to make sure + * that the flush is complete. So we don't have to output an + * empty block here, this will be done at next call. This also + * ensures that for a very small output buffer, we emit at most + * one empty block. + */ + } + if (bstate == block_done) { + if (flush == Z_PARTIAL_FLUSH) { + _tr_align(s); + } else if (flush != Z_BLOCK) { /* FULL_FLUSH or SYNC_FLUSH */ + _tr_stored_block(s, (char*)0, 0L, 0); + /* For a full flush, this empty block will be recognized + * as a special marker by inflate_sync(). + */ + if (flush == Z_FULL_FLUSH) { + CLEAR_HASH(s); /* forget history */ + if (s->lookahead == 0) { + s->strstart = 0; + s->block_start = 0L; + s->insert = 0; + } + } + } + flush_pending(strm); + if (strm->avail_out == 0) { + s->last_flush = -1; /* avoid BUF_ERROR at next call, see above */ + return Z_OK; + } + } + } + + if (flush != Z_FINISH) return Z_OK; + if (s->wrap <= 0) return Z_STREAM_END; + + /* Write the trailer */ +#ifdef GZIP + if (s->wrap == 2) { + put_byte(s, (Byte)(strm->adler & 0xff)); + put_byte(s, (Byte)((strm->adler >> 8) & 0xff)); + put_byte(s, (Byte)((strm->adler >> 16) & 0xff)); + put_byte(s, (Byte)((strm->adler >> 24) & 0xff)); + put_byte(s, (Byte)(strm->total_in & 0xff)); + put_byte(s, (Byte)((strm->total_in >> 8) & 0xff)); + put_byte(s, (Byte)((strm->total_in >> 16) & 0xff)); + put_byte(s, (Byte)((strm->total_in >> 24) & 0xff)); + } + else +#endif + { + putShortMSB(s, (uInt)(strm->adler >> 16)); + putShortMSB(s, (uInt)(strm->adler & 0xffff)); + } + flush_pending(strm); + /* If avail_out is zero, the application will call deflate again + * to flush the rest. + */ + if (s->wrap > 0) s->wrap = -s->wrap; /* write the trailer only once! */ + return s->pending != 0 ? Z_OK : Z_STREAM_END; +} + +/* ========================================================================= */ +int ZEXPORT deflateEnd(z_streamp strm) { + int status; + + if (deflateStateCheck(strm)) return Z_STREAM_ERROR; + + status = strm->state->status; + + /* Deallocate in reverse order of allocations: */ + TRY_FREE(strm, strm->state->pending_buf); + TRY_FREE(strm, strm->state->head); + TRY_FREE(strm, strm->state->prev); + TRY_FREE(strm, strm->state->window); + + ZFREE(strm, strm->state); + strm->state = Z_NULL; + + return status == BUSY_STATE ? Z_DATA_ERROR : Z_OK; +} + +/* ========================================================================= + * Copy the source state to the destination state. + * To simplify the source, this is not supported for 16-bit MSDOS (which + * doesn't have enough memory anyway to duplicate compression states). + */ +int ZEXPORT deflateCopy(z_streamp dest, z_streamp source) { +#ifdef MAXSEG_64K + (void)dest; + (void)source; + return Z_STREAM_ERROR; +#else + deflate_state *ds; + deflate_state *ss; + + + if (deflateStateCheck(source) || dest == Z_NULL) { + return Z_STREAM_ERROR; + } + + ss = source->state; + + zmemcpy(dest, source, sizeof(z_stream)); + + ds = (deflate_state *) ZALLOC(dest, 1, sizeof(deflate_state)); + if (ds == Z_NULL) return Z_MEM_ERROR; + zmemzero(ds, sizeof(deflate_state)); + dest->state = (struct internal_state FAR *) ds; + zmemcpy(ds, ss, sizeof(deflate_state)); + ds->strm = dest; + + ds->window = (Bytef *) ZALLOC(dest, ds->w_size, 2*sizeof(Byte)); + ds->prev = (Posf *) ZALLOC(dest, ds->w_size, sizeof(Pos)); + ds->head = (Posf *) ZALLOC(dest, ds->hash_size, sizeof(Pos)); + ds->pending_buf = (uchf *) ZALLOC(dest, ds->lit_bufsize, LIT_BUFS); + + if (ds->window == Z_NULL || ds->prev == Z_NULL || ds->head == Z_NULL || + ds->pending_buf == Z_NULL) { + deflateEnd (dest); + return Z_MEM_ERROR; + } + /* following zmemcpy's do not work for 16-bit MSDOS */ + zmemcpy(ds->window, ss->window, ss->high_water); + zmemcpy(ds->prev, ss->prev, + (ss->slid || ss->strstart - ss->insert > ds->w_size ? ds->w_size : + ss->strstart - ss->insert) * sizeof(Pos)); + zmemcpy(ds->head, ss->head, ds->hash_size * sizeof(Pos)); + + ds->pending_out = ds->pending_buf + (ss->pending_out - ss->pending_buf); + zmemcpy(ds->pending_out, ss->pending_out, ss->pending); +#ifdef LIT_MEM + ds->d_buf = (ushf *)(ds->pending_buf + (ds->lit_bufsize << 1)); + ds->l_buf = ds->pending_buf + (ds->lit_bufsize << 2); + zmemcpy(ds->d_buf, ss->d_buf, ss->sym_next * sizeof(ush)); + zmemcpy(ds->l_buf, ss->l_buf, ss->sym_next); +#else + ds->sym_buf = ds->pending_buf + ds->lit_bufsize; + zmemcpy(ds->sym_buf, ss->sym_buf, ss->sym_next); +#endif + + ds->l_desc.dyn_tree = ds->dyn_ltree; + ds->d_desc.dyn_tree = ds->dyn_dtree; + ds->bl_desc.dyn_tree = ds->bl_tree; + + return Z_OK; +#endif /* MAXSEG_64K */ +} + +#ifndef FASTEST +/* =========================================================================== + * Set match_start to the longest match starting at the given string and + * return its length. Matches shorter or equal to prev_length are discarded, + * in which case the result is equal to prev_length and match_start is + * garbage. + * IN assertions: cur_match is the head of the hash chain for the current + * string (strstart) and its distance is <= MAX_DIST, and prev_length >= 1 + * OUT assertion: the match length is not greater than s->lookahead. + */ +local uInt longest_match(deflate_state *s, IPos cur_match) { + unsigned chain_length = s->max_chain_length;/* max hash chain length */ + Bytef *scan = s->window + s->strstart; /* current string */ + Bytef *match; /* matched string */ + int len; /* length of current match */ + int best_len = (int)s->prev_length; /* best match length so far */ + int nice_match = s->nice_match; /* stop if match long enough */ + IPos limit = s->strstart > (IPos)MAX_DIST(s) ? + s->strstart - (IPos)MAX_DIST(s) : NIL; + /* Stop when cur_match becomes <= limit. To simplify the code, + * we prevent matches with the string of window index 0. + */ + Posf *prev = s->prev; + uInt wmask = s->w_mask; + +#ifdef UNALIGNED_OK + /* Compare two bytes at a time. Note: this is not always beneficial. + * Try with and without -DUNALIGNED_OK to check. + */ + Bytef *strend = s->window + s->strstart + MAX_MATCH - 1; + ush scan_start = *(ushf*)scan; + ush scan_end = *(ushf*)(scan + best_len - 1); +#else + Bytef *strend = s->window + s->strstart + MAX_MATCH; + Byte scan_end1 = scan[best_len - 1]; + Byte scan_end = scan[best_len]; +#endif + + /* The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16. + * It is easy to get rid of this optimization if necessary. + */ + Assert(s->hash_bits >= 8 && MAX_MATCH == 258, "Code too clever"); + + /* Do not waste too much time if we already have a good match: */ + if (s->prev_length >= s->good_match) { + chain_length >>= 2; + } + /* Do not look for matches beyond the end of the input. This is necessary + * to make deflate deterministic. + */ + if ((uInt)nice_match > s->lookahead) nice_match = (int)s->lookahead; + + Assert((ulg)s->strstart <= s->window_size - MIN_LOOKAHEAD, + "need lookahead"); + + do { + Assert(cur_match < s->strstart, "no future"); + match = s->window + cur_match; + + /* Skip to next match if the match length cannot increase + * or if the match length is less than 2. Note that the checks below + * for insufficient lookahead only occur occasionally for performance + * reasons. Therefore uninitialized memory will be accessed, and + * conditional jumps will be made that depend on those values. + * However the length of the match is limited to the lookahead, so + * the output of deflate is not affected by the uninitialized values. + */ +#if (defined(UNALIGNED_OK) && MAX_MATCH == 258) + /* This code assumes sizeof(unsigned short) == 2. Do not use + * UNALIGNED_OK if your compiler uses a different size. + */ + if (*(ushf*)(match + best_len - 1) != scan_end || + *(ushf*)match != scan_start) continue; + + /* It is not necessary to compare scan[2] and match[2] since they are + * always equal when the other bytes match, given that the hash keys + * are equal and that HASH_BITS >= 8. Compare 2 bytes at a time at + * strstart + 3, + 5, up to strstart + 257. We check for insufficient + * lookahead only every 4th comparison; the 128th check will be made + * at strstart + 257. If MAX_MATCH-2 is not a multiple of 8, it is + * necessary to put more guard bytes at the end of the window, or + * to check more often for insufficient lookahead. + */ + Assert(scan[2] == match[2], "scan[2]?"); + scan++, match++; + do { + } while (*(ushf*)(scan += 2) == *(ushf*)(match += 2) && + *(ushf*)(scan += 2) == *(ushf*)(match += 2) && + *(ushf*)(scan += 2) == *(ushf*)(match += 2) && + *(ushf*)(scan += 2) == *(ushf*)(match += 2) && + scan < strend); + /* The funny "do {}" generates better code on most compilers */ + + /* Here, scan <= window + strstart + 257 */ + Assert(scan <= s->window + (unsigned)(s->window_size - 1), + "wild scan"); + if (*scan == *match) scan++; + + len = (MAX_MATCH - 1) - (int)(strend - scan); + scan = strend - (MAX_MATCH-1); + +#else /* UNALIGNED_OK */ + + if (match[best_len] != scan_end || + match[best_len - 1] != scan_end1 || + *match != *scan || + *++match != scan[1]) continue; + + /* The check at best_len - 1 can be removed because it will be made + * again later. (This heuristic is not always a win.) + * It is not necessary to compare scan[2] and match[2] since they + * are always equal when the other bytes match, given that + * the hash keys are equal and that HASH_BITS >= 8. + */ + scan += 2, match++; + Assert(*scan == *match, "match[2]?"); + + /* We check for insufficient lookahead only every 8th comparison; + * the 256th check will be made at strstart + 258. + */ + do { + } while (*++scan == *++match && *++scan == *++match && + *++scan == *++match && *++scan == *++match && + *++scan == *++match && *++scan == *++match && + *++scan == *++match && *++scan == *++match && + scan < strend); + + Assert(scan <= s->window + (unsigned)(s->window_size - 1), + "wild scan"); + + len = MAX_MATCH - (int)(strend - scan); + scan = strend - MAX_MATCH; + +#endif /* UNALIGNED_OK */ + + if (len > best_len) { + s->match_start = cur_match; + best_len = len; + if (len >= nice_match) break; +#ifdef UNALIGNED_OK + scan_end = *(ushf*)(scan + best_len - 1); +#else + scan_end1 = scan[best_len - 1]; + scan_end = scan[best_len]; +#endif + } + } while ((cur_match = prev[cur_match & wmask]) > limit + && --chain_length != 0); + + if ((uInt)best_len <= s->lookahead) return (uInt)best_len; + return s->lookahead; +} + +#else /* FASTEST */ + +/* --------------------------------------------------------------------------- + * Optimized version for FASTEST only + */ +local uInt longest_match(deflate_state *s, IPos cur_match) { + Bytef *scan = s->window + s->strstart; /* current string */ + Bytef *match; /* matched string */ + int len; /* length of current match */ + Bytef *strend = s->window + s->strstart + MAX_MATCH; + + /* The code is optimized for HASH_BITS >= 8 and MAX_MATCH-2 multiple of 16. + * It is easy to get rid of this optimization if necessary. + */ + Assert(s->hash_bits >= 8 && MAX_MATCH == 258, "Code too clever"); + + Assert((ulg)s->strstart <= s->window_size - MIN_LOOKAHEAD, + "need lookahead"); + + Assert(cur_match < s->strstart, "no future"); + + match = s->window + cur_match; + + /* Return failure if the match length is less than 2: + */ + if (match[0] != scan[0] || match[1] != scan[1]) return MIN_MATCH-1; + + /* The check at best_len - 1 can be removed because it will be made + * again later. (This heuristic is not always a win.) + * It is not necessary to compare scan[2] and match[2] since they + * are always equal when the other bytes match, given that + * the hash keys are equal and that HASH_BITS >= 8. + */ + scan += 2, match += 2; + Assert(*scan == *match, "match[2]?"); + + /* We check for insufficient lookahead only every 8th comparison; + * the 256th check will be made at strstart + 258. + */ + do { + } while (*++scan == *++match && *++scan == *++match && + *++scan == *++match && *++scan == *++match && + *++scan == *++match && *++scan == *++match && + *++scan == *++match && *++scan == *++match && + scan < strend); + + Assert(scan <= s->window + (unsigned)(s->window_size - 1), "wild scan"); + + len = MAX_MATCH - (int)(strend - scan); + + if (len < MIN_MATCH) return MIN_MATCH - 1; + + s->match_start = cur_match; + return (uInt)len <= s->lookahead ? (uInt)len : s->lookahead; +} + +#endif /* FASTEST */ + +#ifdef ZLIB_DEBUG + +#define EQUAL 0 +/* result of memcmp for equal strings */ + +/* =========================================================================== + * Check that the match at match_start is indeed a match. + */ +local void check_match(deflate_state *s, IPos start, IPos match, int length) { + /* check that the match is indeed a match */ + Bytef *back = s->window + (int)match, *here = s->window + start; + IPos len = (IPos)length; + if (match == (IPos)-1) { + /* match starts one byte before the current window -- just compare the + subsequent length-1 bytes */ + back++; + here++; + len--; + } + if (zmemcmp(back, here, len) != EQUAL) { + fprintf(stderr, " start %u, match %d, length %d\n", + start, (int)match, length); + do { + fprintf(stderr, "(%02x %02x)", *back++, *here++); + } while (--len != 0); + z_error("invalid match"); + } + if (z_verbose > 1) { + fprintf(stderr,"\\[%d,%d]", start - match, length); + do { putc(s->window[start++], stderr); } while (--length != 0); + } +} +#else +# define check_match(s, start, match, length) +#endif /* ZLIB_DEBUG */ + +/* =========================================================================== + * Flush the current block, with given end-of-file flag. + * IN assertion: strstart is set to the end of the current match. + */ +#define FLUSH_BLOCK_ONLY(s, last) { \ + _tr_flush_block(s, (s->block_start >= 0L ? \ + (charf *)&s->window[(unsigned)s->block_start] : \ + (charf *)Z_NULL), \ + (ulg)((long)s->strstart - s->block_start), \ + (last)); \ + s->block_start = s->strstart; \ + flush_pending(s->strm); \ + Tracev((stderr,"[FLUSH]")); \ +} + +/* Same but force premature exit if necessary. */ +#define FLUSH_BLOCK(s, last) { \ + FLUSH_BLOCK_ONLY(s, last); \ + if (s->strm->avail_out == 0) return (last) ? finish_started : need_more; \ +} + +/* Maximum stored block length in deflate format (not including header). */ +#define MAX_STORED 65535 + +/* Minimum of a and b. */ +#define MIN(a, b) ((a) > (b) ? (b) : (a)) + +/* =========================================================================== + * Copy without compression as much as possible from the input stream, return + * the current block state. + * + * In case deflateParams() is used to later switch to a non-zero compression + * level, s->matches (otherwise unused when storing) keeps track of the number + * of hash table slides to perform. If s->matches is 1, then one hash table + * slide will be done when switching. If s->matches is 2, the maximum value + * allowed here, then the hash table will be cleared, since two or more slides + * is the same as a clear. + * + * deflate_stored() is written to minimize the number of times an input byte is + * copied. It is most efficient with large input and output buffers, which + * maximizes the opportunities to have a single copy from next_in to next_out. + */ +local block_state deflate_stored(deflate_state *s, int flush) { + /* Smallest worthy block size when not flushing or finishing. By default + * this is 32K. This can be as small as 507 bytes for memLevel == 1. For + * large input and output buffers, the stored block size will be larger. + */ + unsigned min_block = (unsigned)(MIN(s->pending_buf_size - 5, s->w_size)); + + /* Copy as many min_block or larger stored blocks directly to next_out as + * possible. If flushing, copy the remaining available input to next_out as + * stored blocks, if there is enough space. + */ + int last = 0; + unsigned len, left, have; + unsigned used = s->strm->avail_in; + do { + /* Set len to the maximum size block that we can copy directly with the + * available input data and output space. Set left to how much of that + * would be copied from what's left in the window. + */ + len = MAX_STORED; /* maximum deflate stored block length */ + have = ((unsigned)s->bi_valid + 42) >> 3; /* bytes in header */ + if (s->strm->avail_out < have) /* need room for header */ + break; + /* maximum stored block length that will fit in avail_out: */ + have = s->strm->avail_out - have; + left = (unsigned)(s->strstart - s->block_start); /* window bytes */ + if (len > (ulg)left + s->strm->avail_in) + len = left + s->strm->avail_in; /* limit len to the input */ + if (len > have) + len = have; /* limit len to the output */ + + /* If the stored block would be less than min_block in length, or if + * unable to copy all of the available input when flushing, then try + * copying to the window and the pending buffer instead. Also don't + * write an empty block when flushing -- deflate() does that. + */ + if (len < min_block && ((len == 0 && flush != Z_FINISH) || + flush == Z_NO_FLUSH || + len != left + s->strm->avail_in)) + break; + + /* Make a dummy stored block in pending to get the header bytes, + * including any pending bits. This also updates the debugging counts. + */ + last = flush == Z_FINISH && len == left + s->strm->avail_in ? 1 : 0; + _tr_stored_block(s, (char *)0, 0L, last); + + /* Replace the lengths in the dummy stored block with len. */ + s->pending_buf[s->pending - 4] = (Bytef)len; + s->pending_buf[s->pending - 3] = (Bytef)(len >> 8); + s->pending_buf[s->pending - 2] = (Bytef)~len; + s->pending_buf[s->pending - 1] = (Bytef)(~len >> 8); + + /* Write the stored block header bytes. */ + flush_pending(s->strm); + +#ifdef ZLIB_DEBUG + /* Update debugging counts for the data about to be copied. */ + s->compressed_len += len << 3; + s->bits_sent += len << 3; +#endif + + /* Copy uncompressed bytes from the window to next_out. */ + if (left) { + if (left > len) + left = len; + zmemcpy(s->strm->next_out, s->window + s->block_start, left); + s->strm->next_out += left; + s->strm->avail_out -= left; + s->strm->total_out += left; + s->block_start += left; + len -= left; + } + + /* Copy uncompressed bytes directly from next_in to next_out, updating + * the check value. + */ + if (len) { + read_buf(s->strm, s->strm->next_out, len); + s->strm->next_out += len; + s->strm->avail_out -= len; + s->strm->total_out += len; + } + } while (last == 0); + + /* Update the sliding window with the last s->w_size bytes of the copied + * data, or append all of the copied data to the existing window if less + * than s->w_size bytes were copied. Also update the number of bytes to + * insert in the hash tables, in the event that deflateParams() switches to + * a non-zero compression level. + */ + used -= s->strm->avail_in; /* number of input bytes directly copied */ + if (used) { + /* If any input was used, then no unused input remains in the window, + * therefore s->block_start == s->strstart. + */ + if (used >= s->w_size) { /* supplant the previous history */ + s->matches = 2; /* clear hash */ + zmemcpy(s->window, s->strm->next_in - s->w_size, s->w_size); + s->strstart = s->w_size; + s->insert = s->strstart; + } + else { + if (s->window_size - s->strstart <= used) { + /* Slide the window down. */ + s->strstart -= s->w_size; + zmemcpy(s->window, s->window + s->w_size, s->strstart); + if (s->matches < 2) + s->matches++; /* add a pending slide_hash() */ + if (s->insert > s->strstart) + s->insert = s->strstart; + } + zmemcpy(s->window + s->strstart, s->strm->next_in - used, used); + s->strstart += used; + s->insert += MIN(used, s->w_size - s->insert); + } + s->block_start = s->strstart; + } + if (s->high_water < s->strstart) + s->high_water = s->strstart; + + /* If the last block was written to next_out, then done. */ + if (last) { + s->bi_used = 8; + return finish_done; + } + + /* If flushing and all input has been consumed, then done. */ + if (flush != Z_NO_FLUSH && flush != Z_FINISH && + s->strm->avail_in == 0 && (long)s->strstart == s->block_start) + return block_done; + + /* Fill the window with any remaining input. */ + have = (unsigned)(s->window_size - s->strstart); + if (s->strm->avail_in > have && s->block_start >= (long)s->w_size) { + /* Slide the window down. */ + s->block_start -= s->w_size; + s->strstart -= s->w_size; + zmemcpy(s->window, s->window + s->w_size, s->strstart); + if (s->matches < 2) + s->matches++; /* add a pending slide_hash() */ + have += s->w_size; /* more space now */ + if (s->insert > s->strstart) + s->insert = s->strstart; + } + if (have > s->strm->avail_in) + have = s->strm->avail_in; + if (have) { + read_buf(s->strm, s->window + s->strstart, have); + s->strstart += have; + s->insert += MIN(have, s->w_size - s->insert); + } + if (s->high_water < s->strstart) + s->high_water = s->strstart; + + /* There was not enough avail_out to write a complete worthy or flushed + * stored block to next_out. Write a stored block to pending instead, if we + * have enough input for a worthy block, or if flushing and there is enough + * room for the remaining input as a stored block in the pending buffer. + */ + have = ((unsigned)s->bi_valid + 42) >> 3; /* bytes in header */ + /* maximum stored block length that will fit in pending: */ + have = (unsigned)MIN(s->pending_buf_size - have, MAX_STORED); + min_block = MIN(have, s->w_size); + left = (unsigned)(s->strstart - s->block_start); + if (left >= min_block || + ((left || flush == Z_FINISH) && flush != Z_NO_FLUSH && + s->strm->avail_in == 0 && left <= have)) { + len = MIN(left, have); + last = flush == Z_FINISH && s->strm->avail_in == 0 && + len == left ? 1 : 0; + _tr_stored_block(s, (charf *)s->window + s->block_start, len, last); + s->block_start += len; + flush_pending(s->strm); + } + + /* We've done all we can with the available input and output. */ + if (last) + s->bi_used = 8; + return last ? finish_started : need_more; +} + +/* =========================================================================== + * Compress as much as possible from the input stream, return the current + * block state. + * This function does not perform lazy evaluation of matches and inserts + * new strings in the dictionary only for unmatched strings or for short + * matches. It is used only for the fast compression options. + */ +local block_state deflate_fast(deflate_state *s, int flush) { + IPos hash_head; /* head of the hash chain */ + int bflush; /* set if current block must be flushed */ + + for (;;) { + /* Make sure that we always have enough lookahead, except + * at the end of the input file. We need MAX_MATCH bytes + * for the next match, plus MIN_MATCH bytes to insert the + * string following the next match. + */ + if (s->lookahead < MIN_LOOKAHEAD) { + fill_window(s); + if (s->lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) { + return need_more; + } + if (s->lookahead == 0) break; /* flush the current block */ + } + + /* Insert the string window[strstart .. strstart + 2] in the + * dictionary, and set hash_head to the head of the hash chain: + */ + hash_head = NIL; + if (s->lookahead >= MIN_MATCH) { + INSERT_STRING(s, s->strstart, hash_head); + } + + /* Find the longest match, discarding those <= prev_length. + * At this point we have always match_length < MIN_MATCH + */ + if (hash_head != NIL && s->strstart - hash_head <= MAX_DIST(s)) { + /* To simplify the code, we prevent matches with the string + * of window index 0 (in particular we have to avoid a match + * of the string with itself at the start of the input file). + */ + s->match_length = longest_match (s, hash_head); + /* longest_match() sets match_start */ + } + if (s->match_length >= MIN_MATCH) { + check_match(s, s->strstart, s->match_start, (int)s->match_length); + + _tr_tally_dist(s, s->strstart - s->match_start, + s->match_length - MIN_MATCH, bflush); + + s->lookahead -= s->match_length; + + /* Insert new strings in the hash table only if the match length + * is not too large. This saves time but degrades compression. + */ +#ifndef FASTEST + if (s->match_length <= s->max_insert_length && + s->lookahead >= MIN_MATCH) { + s->match_length--; /* string at strstart already in table */ + do { + s->strstart++; + INSERT_STRING(s, s->strstart, hash_head); + /* strstart never exceeds WSIZE-MAX_MATCH, so there are + * always MIN_MATCH bytes ahead. + */ + } while (--s->match_length != 0); + s->strstart++; + } else +#endif + { + s->strstart += s->match_length; + s->match_length = 0; + s->ins_h = s->window[s->strstart]; + UPDATE_HASH(s, s->ins_h, s->window[s->strstart + 1]); +#if MIN_MATCH != 3 + Call UPDATE_HASH() MIN_MATCH-3 more times +#endif + /* If lookahead < MIN_MATCH, ins_h is garbage, but it does not + * matter since it will be recomputed at next deflate call. + */ + } + } else { + /* No match, output a literal byte */ + Tracevv((stderr,"%c", s->window[s->strstart])); + _tr_tally_lit(s, s->window[s->strstart], bflush); + s->lookahead--; + s->strstart++; + } + if (bflush) FLUSH_BLOCK(s, 0); + } + s->insert = s->strstart < MIN_MATCH-1 ? s->strstart : MIN_MATCH-1; + if (flush == Z_FINISH) { + FLUSH_BLOCK(s, 1); + return finish_done; + } + if (s->sym_next) + FLUSH_BLOCK(s, 0); + return block_done; +} + +#ifndef FASTEST +/* =========================================================================== + * Same as above, but achieves better compression. We use a lazy + * evaluation for matches: a match is finally adopted only if there is + * no better match at the next window position. + */ +local block_state deflate_slow(deflate_state *s, int flush) { + IPos hash_head; /* head of hash chain */ + int bflush; /* set if current block must be flushed */ + + /* Process the input block. */ + for (;;) { + /* Make sure that we always have enough lookahead, except + * at the end of the input file. We need MAX_MATCH bytes + * for the next match, plus MIN_MATCH bytes to insert the + * string following the next match. + */ + if (s->lookahead < MIN_LOOKAHEAD) { + fill_window(s); + if (s->lookahead < MIN_LOOKAHEAD && flush == Z_NO_FLUSH) { + return need_more; + } + if (s->lookahead == 0) break; /* flush the current block */ + } + + /* Insert the string window[strstart .. strstart + 2] in the + * dictionary, and set hash_head to the head of the hash chain: + */ + hash_head = NIL; + if (s->lookahead >= MIN_MATCH) { + INSERT_STRING(s, s->strstart, hash_head); + } + + /* Find the longest match, discarding those <= prev_length. + */ + s->prev_length = s->match_length, s->prev_match = s->match_start; + s->match_length = MIN_MATCH-1; + + if (hash_head != NIL && s->prev_length < s->max_lazy_match && + s->strstart - hash_head <= MAX_DIST(s)) { + /* To simplify the code, we prevent matches with the string + * of window index 0 (in particular we have to avoid a match + * of the string with itself at the start of the input file). + */ + s->match_length = longest_match (s, hash_head); + /* longest_match() sets match_start */ + + if (s->match_length <= 5 && (s->strategy == Z_FILTERED +#if TOO_FAR <= 32767 + || (s->match_length == MIN_MATCH && + s->strstart - s->match_start > TOO_FAR) +#endif + )) { + + /* If prev_match is also MIN_MATCH, match_start is garbage + * but we will ignore the current match anyway. + */ + s->match_length = MIN_MATCH-1; + } + } + /* If there was a match at the previous step and the current + * match is not better, output the previous match: + */ + if (s->prev_length >= MIN_MATCH && s->match_length <= s->prev_length) { + uInt max_insert = s->strstart + s->lookahead - MIN_MATCH; + /* Do not insert strings in hash table beyond this. */ + + check_match(s, s->strstart - 1, s->prev_match, (int)s->prev_length); + + _tr_tally_dist(s, s->strstart - 1 - s->prev_match, + s->prev_length - MIN_MATCH, bflush); + + /* Insert in hash table all strings up to the end of the match. + * strstart - 1 and strstart are already inserted. If there is not + * enough lookahead, the last two strings are not inserted in + * the hash table. + */ + s->lookahead -= s->prev_length - 1; + s->prev_length -= 2; + do { + if (++s->strstart <= max_insert) { + INSERT_STRING(s, s->strstart, hash_head); + } + } while (--s->prev_length != 0); + s->match_available = 0; + s->match_length = MIN_MATCH-1; + s->strstart++; + + if (bflush) FLUSH_BLOCK(s, 0); + + } else if (s->match_available) { + /* If there was no match at the previous position, output a + * single literal. If there was a match but the current match + * is longer, truncate the previous match to a single literal. + */ + Tracevv((stderr,"%c", s->window[s->strstart - 1])); + _tr_tally_lit(s, s->window[s->strstart - 1], bflush); + if (bflush) { + FLUSH_BLOCK_ONLY(s, 0); + } + s->strstart++; + s->lookahead--; + if (s->strm->avail_out == 0) return need_more; + } else { + /* There is no previous match to compare with, wait for + * the next step to decide. + */ + s->match_available = 1; + s->strstart++; + s->lookahead--; + } + } + Assert (flush != Z_NO_FLUSH, "no flush?"); + if (s->match_available) { + Tracevv((stderr,"%c", s->window[s->strstart - 1])); + _tr_tally_lit(s, s->window[s->strstart - 1], bflush); + s->match_available = 0; + } + s->insert = s->strstart < MIN_MATCH-1 ? s->strstart : MIN_MATCH-1; + if (flush == Z_FINISH) { + FLUSH_BLOCK(s, 1); + return finish_done; + } + if (s->sym_next) + FLUSH_BLOCK(s, 0); + return block_done; +} +#endif /* FASTEST */ + +/* =========================================================================== + * For Z_RLE, simply look for runs of bytes, generate matches only of distance + * one. Do not maintain a hash table. (It will be regenerated if this run of + * deflate switches away from Z_RLE.) + */ +local block_state deflate_rle(deflate_state *s, int flush) { + int bflush; /* set if current block must be flushed */ + uInt prev; /* byte at distance one to match */ + Bytef *scan, *strend; /* scan goes up to strend for length of run */ + + for (;;) { + /* Make sure that we always have enough lookahead, except + * at the end of the input file. We need MAX_MATCH bytes + * for the longest run, plus one for the unrolled loop. + */ + if (s->lookahead <= MAX_MATCH) { + fill_window(s); + if (s->lookahead <= MAX_MATCH && flush == Z_NO_FLUSH) { + return need_more; + } + if (s->lookahead == 0) break; /* flush the current block */ + } + + /* See how many times the previous byte repeats */ + s->match_length = 0; + if (s->lookahead >= MIN_MATCH && s->strstart > 0) { + scan = s->window + s->strstart - 1; + prev = *scan; + if (prev == *++scan && prev == *++scan && prev == *++scan) { + strend = s->window + s->strstart + MAX_MATCH; + do { + } while (prev == *++scan && prev == *++scan && + prev == *++scan && prev == *++scan && + prev == *++scan && prev == *++scan && + prev == *++scan && prev == *++scan && + scan < strend); + s->match_length = MAX_MATCH - (uInt)(strend - scan); + if (s->match_length > s->lookahead) + s->match_length = s->lookahead; + } + Assert(scan <= s->window + (uInt)(s->window_size - 1), + "wild scan"); + } + + /* Emit match if have run of MIN_MATCH or longer, else emit literal */ + if (s->match_length >= MIN_MATCH) { + check_match(s, s->strstart, s->strstart - 1, (int)s->match_length); + + _tr_tally_dist(s, 1, s->match_length - MIN_MATCH, bflush); + + s->lookahead -= s->match_length; + s->strstart += s->match_length; + s->match_length = 0; + } else { + /* No match, output a literal byte */ + Tracevv((stderr,"%c", s->window[s->strstart])); + _tr_tally_lit(s, s->window[s->strstart], bflush); + s->lookahead--; + s->strstart++; + } + if (bflush) FLUSH_BLOCK(s, 0); + } + s->insert = 0; + if (flush == Z_FINISH) { + FLUSH_BLOCK(s, 1); + return finish_done; + } + if (s->sym_next) + FLUSH_BLOCK(s, 0); + return block_done; +} + +/* =========================================================================== + * For Z_HUFFMAN_ONLY, do not look for matches. Do not maintain a hash table. + * (It will be regenerated if this run of deflate switches away from Huffman.) + */ +local block_state deflate_huff(deflate_state *s, int flush) { + int bflush; /* set if current block must be flushed */ + + for (;;) { + /* Make sure that we have a literal to write. */ + if (s->lookahead == 0) { + fill_window(s); + if (s->lookahead == 0) { + if (flush == Z_NO_FLUSH) + return need_more; + break; /* flush the current block */ + } + } + + /* Output a literal byte */ + s->match_length = 0; + Tracevv((stderr,"%c", s->window[s->strstart])); + _tr_tally_lit(s, s->window[s->strstart], bflush); + s->lookahead--; + s->strstart++; + if (bflush) FLUSH_BLOCK(s, 0); + } + s->insert = 0; + if (flush == Z_FINISH) { + FLUSH_BLOCK(s, 1); + return finish_done; + } + if (s->sym_next) + FLUSH_BLOCK(s, 0); + return block_done; +} diff --git a/compat/zlib/deflate.h b/compat/zlib/deflate.h new file mode 100644 index 0000000..0732ba8 --- /dev/null +++ b/compat/zlib/deflate.h @@ -0,0 +1,383 @@ +/* deflate.h -- internal compression state + * Copyright (C) 1995-2026 Jean-loup Gailly + * For conditions of distribution and use, see copyright notice in zlib.h + */ + +/* WARNING: this file should *not* be used by applications. It is + part of the implementation of the compression library and is + subject to change. Applications should only use zlib.h. + */ + +/* @(#) $Id$ */ + +#ifndef DEFLATE_H +#define DEFLATE_H + +#include "zutil.h" + +/* define NO_GZIP when compiling if you want to disable gzip header and + trailer creation by deflate(). NO_GZIP would be used to avoid linking in + the crc code when it is not needed. For shared libraries, gzip encoding + should be left enabled. */ +#ifndef NO_GZIP +# define GZIP +#endif + +/* define LIT_MEM to slightly increase the speed of deflate (order 1% to 2%) at + the cost of a larger memory footprint */ +/* #define LIT_MEM */ + +/* =========================================================================== + * Internal compression state. + */ + +#define LENGTH_CODES 29 +/* number of length codes, not counting the special END_BLOCK code */ + +#define LITERALS 256 +/* number of literal bytes 0..255 */ + +#define L_CODES (LITERALS+1+LENGTH_CODES) +/* number of Literal or Length codes, including the END_BLOCK code */ + +#define D_CODES 30 +/* number of distance codes */ + +#define BL_CODES 19 +/* number of codes used to transfer the bit lengths */ + +#define HEAP_SIZE (2*L_CODES+1) +/* maximum heap size */ + +#define MAX_BITS 15 +/* All codes must not exceed MAX_BITS bits */ + +#define Buf_size 16 +/* size of bit buffer in bi_buf */ + +#define INIT_STATE 42 /* zlib header -> BUSY_STATE */ +#ifdef GZIP +# define GZIP_STATE 57 /* gzip header -> BUSY_STATE | EXTRA_STATE */ +#endif +#define EXTRA_STATE 69 /* gzip extra block -> NAME_STATE */ +#define NAME_STATE 73 /* gzip file name -> COMMENT_STATE */ +#define COMMENT_STATE 91 /* gzip comment -> HCRC_STATE */ +#define HCRC_STATE 103 /* gzip header CRC -> BUSY_STATE */ +#define BUSY_STATE 113 /* deflate -> FINISH_STATE */ +#define FINISH_STATE 666 /* stream complete */ +/* Stream status */ + + +/* Data structure describing a single value and its code string. */ +typedef struct ct_data_s { + union { + ush freq; /* frequency count */ + ush code; /* bit string */ + } fc; + union { + ush dad; /* father node in Huffman tree */ + ush len; /* length of bit string */ + } dl; +} FAR ct_data; + +#define Freq fc.freq +#define Code fc.code +#define Dad dl.dad +#define Len dl.len + +typedef struct static_tree_desc_s static_tree_desc; + +typedef struct tree_desc_s { + ct_data *dyn_tree; /* the dynamic tree */ + int max_code; /* largest code with non zero frequency */ + const static_tree_desc *stat_desc; /* the corresponding static tree */ +} FAR tree_desc; + +typedef ush Pos; +typedef Pos FAR Posf; +typedef unsigned IPos; + +/* A Pos is an index in the character window. We use short instead of int to + * save space in the various tables. IPos is used only for parameter passing. + */ + +typedef struct internal_state { + z_streamp strm; /* pointer back to this zlib stream */ + int status; /* as the name implies */ + Bytef *pending_buf; /* output still pending */ + ulg pending_buf_size; /* size of pending_buf */ + Bytef *pending_out; /* next pending byte to output to the stream */ + ulg pending; /* nb of bytes in the pending buffer */ + int wrap; /* bit 0 true for zlib, bit 1 true for gzip */ + gz_headerp gzhead; /* gzip header information to write */ + ulg gzindex; /* where in extra, name, or comment */ + Byte method; /* can only be DEFLATED */ + int last_flush; /* value of flush param for previous deflate call */ + + /* used by deflate.c: */ + + uInt w_size; /* LZ77 window size (32K by default) */ + uInt w_bits; /* log2(w_size) (8..16) */ + uInt w_mask; /* w_size - 1 */ + + Bytef *window; + /* Sliding window. Input bytes are read into the second half of the window, + * and move to the first half later to keep a dictionary of at least wSize + * bytes. With this organization, matches are limited to a distance of + * wSize-MAX_MATCH bytes, but this ensures that IO is always + * performed with a length multiple of the block size. Also, it limits + * the window size to 64K, which is quite useful on MSDOS. + * To do: use the user input buffer as sliding window. + */ + + ulg window_size; + /* Actual size of window: 2*wSize, except when the user input buffer + * is directly used as sliding window. + */ + + Posf *prev; + /* Link to older string with same hash index. To limit the size of this + * array to 64K, this link is maintained only for the last 32K strings. + * An index in this array is thus a window index modulo 32K. + */ + + Posf *head; /* Heads of the hash chains or NIL. */ + + uInt ins_h; /* hash index of string to be inserted */ + uInt hash_size; /* number of elements in hash table */ + uInt hash_bits; /* log2(hash_size) */ + uInt hash_mask; /* hash_size-1 */ + + uInt hash_shift; + /* Number of bits by which ins_h must be shifted at each input + * step. It must be such that after MIN_MATCH steps, the oldest + * byte no longer takes part in the hash key, that is: + * hash_shift * MIN_MATCH >= hash_bits + */ + + long block_start; + /* Window position at the beginning of the current output block. Gets + * negative when the window is moved backwards. + */ + + uInt match_length; /* length of best match */ + IPos prev_match; /* previous match */ + int match_available; /* set if previous match exists */ + uInt strstart; /* start of string to insert */ + uInt match_start; /* start of matching string */ + uInt lookahead; /* number of valid bytes ahead in window */ + + uInt prev_length; + /* Length of the best match at previous step. Matches not greater than this + * are discarded. This is used in the lazy match evaluation. + */ + + uInt max_chain_length; + /* To speed up deflation, hash chains are never searched beyond this + * length. A higher limit improves compression ratio but degrades the + * speed. + */ + + uInt max_lazy_match; + /* Attempt to find a better match only when the current match is strictly + * smaller than this value. This mechanism is used only for compression + * levels >= 4. + */ +# define max_insert_length max_lazy_match + /* Insert new strings in the hash table only if the match length is not + * greater than this length. This saves time but degrades compression. + * max_insert_length is used only for compression levels <= 3. + */ + + int level; /* compression level (1..9) */ + int strategy; /* favor or force Huffman coding*/ + + uInt good_match; + /* Use a faster search when the previous match is longer than this */ + + int nice_match; /* Stop searching when current match exceeds this */ + + /* used by trees.c: */ + /* Didn't use ct_data typedef below to suppress compiler warning */ + struct ct_data_s dyn_ltree[HEAP_SIZE]; /* literal and length tree */ + struct ct_data_s dyn_dtree[2*D_CODES+1]; /* distance tree */ + struct ct_data_s bl_tree[2*BL_CODES+1]; /* Huffman tree for bit lengths */ + + struct tree_desc_s l_desc; /* desc. for literal tree */ + struct tree_desc_s d_desc; /* desc. for distance tree */ + struct tree_desc_s bl_desc; /* desc. for bit length tree */ + + ush bl_count[MAX_BITS+1]; + /* number of codes at each bit length for an optimal tree */ + + int heap[2*L_CODES+1]; /* heap used to build the Huffman trees */ + int heap_len; /* number of elements in the heap */ + int heap_max; /* element of largest frequency */ + /* The sons of heap[n] are heap[2*n] and heap[2*n+1]. heap[0] is not used. + * The same heap array is used to build all trees. + */ + + uch depth[2*L_CODES+1]; + /* Depth of each subtree used as tie breaker for trees of equal frequency + */ + +#ifdef LIT_MEM +# define LIT_BUFS 5 + ushf *d_buf; /* buffer for distances */ + uchf *l_buf; /* buffer for literals/lengths */ +#else +# define LIT_BUFS 4 + uchf *sym_buf; /* buffer for distances and literals/lengths */ +#endif + + uInt lit_bufsize; + /* Size of match buffer for literals/lengths. There are 4 reasons for + * limiting lit_bufsize to 64K: + * - frequencies can be kept in 16 bit counters + * - if compression is not successful for the first block, all input + * data is still in the window so we can still emit a stored block even + * when input comes from standard input. (This can also be done for + * all blocks if lit_bufsize is not greater than 32K.) + * - if compression is not successful for a file smaller than 64K, we can + * even emit a stored file instead of a stored block (saving 5 bytes). + * This is applicable only for zip (not gzip or zlib). + * - creating new Huffman trees less frequently may not provide fast + * adaptation to changes in the input data statistics. (Take for + * example a binary file with poorly compressible code followed by + * a highly compressible string table.) Smaller buffer sizes give + * fast adaptation but have of course the overhead of transmitting + * trees more frequently. + * - I can't count above 4 + */ + + uInt sym_next; /* running index in symbol buffer */ + uInt sym_end; /* symbol table full when sym_next reaches this */ + + ulg opt_len; /* bit length of current block with optimal trees */ + ulg static_len; /* bit length of current block with static trees */ + uInt matches; /* number of string matches in current block */ + uInt insert; /* bytes at end of window left to insert */ + +#ifdef ZLIB_DEBUG + ulg compressed_len; /* total bit length of compressed file mod 2^32 */ + ulg bits_sent; /* bit length of compressed data sent mod 2^32 */ +#endif + + ush bi_buf; + /* Output buffer. bits are inserted starting at the bottom (least + * significant bits). + */ + int bi_valid; + /* Number of valid bits in bi_buf. All bits above the last valid bit + * are always zero. + */ + int bi_used; + /* Last number of used bits when going to a byte boundary. + */ + + ulg high_water; + /* High water mark offset in window for initialized bytes -- bytes above + * this are set to zero in order to avoid memory check warnings when + * longest match routines access bytes past the input. This is then + * updated to the new high water mark. + */ + + int slid; + /* True if the hash table has been slid since it was cleared. */ + +} FAR deflate_state; + +/* Output a byte on the stream. + * IN assertion: there is enough room in pending_buf. + */ +#define put_byte(s, c) {s->pending_buf[s->pending++] = (Bytef)(c);} + + +#define MIN_LOOKAHEAD (MAX_MATCH+MIN_MATCH+1) +/* Minimum amount of lookahead, except at the end of the input file. + * See deflate.c for comments about the MIN_MATCH+1. + */ + +#define MAX_DIST(s) ((s)->w_size-MIN_LOOKAHEAD) +/* In order to simplify the code, particularly on 16 bit machines, match + * distances are limited to MAX_DIST instead of WSIZE. + */ + +#define WIN_INIT MAX_MATCH +/* Number of bytes after end of data in window to initialize in order to avoid + memory checker errors from longest match routines */ + + /* in trees.c */ +void ZLIB_INTERNAL _tr_init(deflate_state *s); +int ZLIB_INTERNAL _tr_tally(deflate_state *s, unsigned dist, unsigned lc); +void ZLIB_INTERNAL _tr_flush_block(deflate_state *s, charf *buf, + ulg stored_len, int last); +void ZLIB_INTERNAL _tr_flush_bits(deflate_state *s); +void ZLIB_INTERNAL _tr_align(deflate_state *s); +void ZLIB_INTERNAL _tr_stored_block(deflate_state *s, charf *buf, + ulg stored_len, int last); + +#define d_code(dist) \ + ((dist) < 256 ? _dist_code[dist] : _dist_code[256+((dist)>>7)]) +/* Mapping from a distance to a distance code. dist is the distance - 1 and + * must not have side effects. _dist_code[256] and _dist_code[257] are never + * used. + */ + +#ifndef ZLIB_DEBUG +/* Inline versions of _tr_tally for speed: */ + +#if defined(GEN_TREES_H) || !defined(STDC) + extern uch ZLIB_INTERNAL _length_code[]; + extern uch ZLIB_INTERNAL _dist_code[]; +#else + extern const uch ZLIB_INTERNAL _length_code[]; + extern const uch ZLIB_INTERNAL _dist_code[]; +#endif + +#ifdef LIT_MEM +# define _tr_tally_lit(s, c, flush) \ + { uch cc = (c); \ + s->d_buf[s->sym_next] = 0; \ + s->l_buf[s->sym_next++] = cc; \ + s->dyn_ltree[cc].Freq++; \ + flush = (s->sym_next == s->sym_end); \ + } +# define _tr_tally_dist(s, distance, length, flush) \ + { uch len = (uch)(length); \ + ush dist = (ush)(distance); \ + s->d_buf[s->sym_next] = dist; \ + s->l_buf[s->sym_next++] = len; \ + dist--; \ + s->dyn_ltree[_length_code[len]+LITERALS+1].Freq++; \ + s->dyn_dtree[d_code(dist)].Freq++; \ + flush = (s->sym_next == s->sym_end); \ + } +#else +# define _tr_tally_lit(s, c, flush) \ + { uch cc = (c); \ + s->sym_buf[s->sym_next++] = 0; \ + s->sym_buf[s->sym_next++] = 0; \ + s->sym_buf[s->sym_next++] = cc; \ + s->dyn_ltree[cc].Freq++; \ + flush = (s->sym_next == s->sym_end); \ + } +# define _tr_tally_dist(s, distance, length, flush) \ + { uch len = (uch)(length); \ + ush dist = (ush)(distance); \ + s->sym_buf[s->sym_next++] = (uch)dist; \ + s->sym_buf[s->sym_next++] = (uch)(dist >> 8); \ + s->sym_buf[s->sym_next++] = len; \ + dist--; \ + s->dyn_ltree[_length_code[len]+LITERALS+1].Freq++; \ + s->dyn_dtree[d_code(dist)].Freq++; \ + flush = (s->sym_next == s->sym_end); \ + } +#endif +#else +# define _tr_tally_lit(s, c, flush) flush = _tr_tally(s, 0, c) +# define _tr_tally_dist(s, distance, length, flush) \ + flush = _tr_tally(s, distance, length) +#endif + +#endif /* DEFLATE_H */ diff --git a/npins/default.nix b/npins/default.nix new file mode 100644 index 0000000..8ec5eca --- /dev/null +++ b/npins/default.nix @@ -0,0 +1,260 @@ +/* + This file is provided under the MIT licence: + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +# Generated by npins. Do not modify; will be overwritten regularly +let + # Backwards-compatibly make something that previously didn't take any arguments take some + # The function must return an attrset, and will unfortunately be eagerly evaluated + # Same thing, but it catches eval errors on the default argument so that one may still call it with other arguments + mkFunctor = + fn: + let + e = builtins.tryEval (fn { }); + in + (if e.success then e.value else { error = fn { }; }) // { __functor = _self: fn; }; + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 + range = + first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 + stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 + stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); + concatStrings = builtins.concatStringsSep ""; + + # If the environment variable NPINS_OVERRIDE_${name} is set, then use + # the path directly as opposed to the fetched source. + # (Taken from Niv for compatibility) + mayOverride = + name: path: + let + envVarName = "NPINS_OVERRIDE_${saneName}"; + saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; + ersatz = builtins.getEnv envVarName; + in + if ersatz == "" then + path + else + # this turns the string into an actual Nix path (for both absolute and + # relative paths) + builtins.trace "Overriding path of \"${name}\" with \"${ersatz}\" due to set \"${envVarName}\"" ( + if builtins.substring 0 1 ersatz == "/" then + /. + ersatz + else + /. + builtins.getEnv "PWD" + "/${ersatz}" + ); + + mkSource = + name: spec: + { + pkgs ? null, + }: + assert spec ? type; + let + # Unify across builtin and pkgs fetchers. + # `fetchGit` requires a wrapper because of slight API differences. + fetchers = + if pkgs == null then + { + inherit (builtins) fetchTarball fetchurl; + # Frustratingly, due to flakes and `fetchTree`, `fetchGit` + # has a different signature than the other builtin + # fetchers + fetchGit = args: (builtins.fetchGit args).outPath; + } + else + { + fetchTarball = + { + url, + sha256, + }: + pkgs.fetchzip { + inherit url sha256; + extension = "tar"; + }; + inherit (pkgs) fetchurl; + fetchGit = + { + url, + submodules, + rev, + name, + narHash, + }: + pkgs.fetchgit { + inherit url rev name; + fetchSubmodules = submodules; + hash = narHash; + }; + }; + + path = + if spec.type == "Git" then + mkGitSource fetchers spec + else if spec.type == "GitRelease" then + mkGitSource fetchers spec + else if spec.type == "PyPi" then + mkPyPiSource fetchers spec + else if spec.type == "Channel" then + mkChannelSource fetchers spec + else if spec.type == "Url" || spec.type == "MutableUrl" then + mkUrlSource fetchers spec + else if spec.type == "Container" then + mkContainerSource pkgs spec + else + builtins.throw "Unknown source type ${spec.type}"; + in + spec // { outPath = mayOverride name path; }; + + mkGitSource = + { + fetchTarball, + fetchGit, + ... + }: + { + repository, + revision, + url ? null, + submodules, + hash, + ... + }: + assert repository ? type; + # At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository + # In the latter case, there we will always be an url to the tarball + if url != null && !submodules then + fetchTarball { + inherit url; + sha256 = hash; + } + else + let + url = + if repository.type == "Git" then + repository.url + else if repository.type == "GitHub" then + "https://github.com/${repository.owner}/${repository.repo}.git" + else if repository.type == "GitLab" then + "${repository.server}/${repository.repo_path}.git" + else if repository.type == "Forgejo" then + "${repository.server}/${repository.owner}/${repository.repo}.git" + else + throw "Unrecognized repository type ${repository.type}"; + urlToName = + url: rev: + let + matched = builtins.match "^.*/([^/]*)(\\.git)?$" url; + + short = builtins.substring 0 7 rev; + + appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else ""; + in + "${if matched == null then "source" else builtins.head matched}${appendShort}"; + name = urlToName url revision; + in + fetchGit { + rev = revision; + narHash = hash; + + inherit name submodules url; + }; + + mkPyPiSource = + { fetchurl, ... }: + { + url, + hash, + ... + }: + fetchurl { + inherit url; + sha256 = hash; + }; + + mkChannelSource = + { fetchTarball, ... }: + { + url, + hash, + ... + }: + fetchTarball { + inherit url; + sha256 = hash; + }; + + mkUrlSource = + { + fetchTarball, + fetchurl, + ... + }: + { + url, + hash, + unpack, + ... + }: + (if unpack then fetchTarball else fetchurl) { + inherit url; + sha256 = hash; + }; + + mkContainerSource = + pkgs: + { + image_name, + image_tag, + image_digest, + hash, + ... + }@args: + if pkgs == null then + builtins.throw "container sources require passing in a Nixpkgs value: https://github.com/andir/npins/blob/master/README.md#using-the-nixpkgs-fetchers" + else + pkgs.dockerTools.pullImage ( + { + imageName = image_name; + imageDigest = image_digest; + finalImageTag = image_tag; + hash = hash; + } + // (if args.arch or null != null then { arch = args.arch; } else { }) + ); + +in +mkFunctor ( + { + input ? ./sources.json, + }: + let + data = + if builtins.isPath input then + # while `readFile` will throw an error anyways if the path doesn't exist, + # we still need to check beforehand because *our* error can be caught but not the one from the builtin + # See: + if builtins.pathExists input then + builtins.fromJSON (builtins.readFile input) + else + throw "Input path ${toString input} does not exist" + else if builtins.isAttrs input then + input + else + throw "Unsupported input type ${builtins.typeOf input}, must be a path or an attrset"; + version = data.version; + in + if version == 8 then + builtins.mapAttrs (name: spec: mkFunctor (mkSource name spec)) data.pins + else + throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`" +) diff --git a/skins/ardoise/details.txt b/skins/ardoise/details.txt new file mode 100644 index 0000000..099dfb8 --- /dev/null +++ b/skins/ardoise/details.txt @@ -0,0 +1,5 @@ +timeline-arrowheads: 0 +timeline-circle-nodes: 1 +timeline-color-graph-lines: 1 +white-foreground: 1 +pikchr-background: 0x1d2021 diff --git a/skins/ardoise/images/delindent.svg b/skins/ardoise/images/delindent.svg new file mode 100644 index 0000000..3c21b81 --- /dev/null +++ b/skins/ardoise/images/delindent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/skins/ardoise/images/delindent_h.svg b/skins/ardoise/images/delindent_h.svg new file mode 100644 index 0000000..bee9f4f --- /dev/null +++ b/skins/ardoise/images/delindent_h.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/skins/ardoise/images/dir.svg b/skins/ardoise/images/dir.svg new file mode 100644 index 0000000..05a2b7b --- /dev/null +++ b/skins/ardoise/images/dir.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/skins/black_and_white/details.txt b/skins/black_and_white/details.txt new file mode 100644 index 0000000..3e05845 --- /dev/null +++ b/skins/black_and_white/details.txt @@ -0,0 +1,4 @@ +timeline-arrowheads: 1 +timeline-circle-nodes: 0 +timeline-color-graph-lines: 0 +white-foreground: 0 diff --git a/skins/blitz/details.txt b/skins/blitz/details.txt new file mode 100644 index 0000000..2c37fca --- /dev/null +++ b/skins/blitz/details.txt @@ -0,0 +1,4 @@ +timeline-arrowheads: 0 +timeline-circle-nodes: 1 +timeline-color-graph-lines: 1 +white-foreground: 0 diff --git a/skins/blitz/dir.png b/skins/blitz/dir.png new file mode 100644 index 0000000..f25f5c5 Binary files /dev/null and b/skins/blitz/dir.png differ diff --git a/skins/copland/details.txt b/skins/copland/details.txt new file mode 100644 index 0000000..17758f6 --- /dev/null +++ b/skins/copland/details.txt @@ -0,0 +1,9 @@ +pikchr-background: "" +pikchr-fontscale: "0.9" +pikchr-foreground: "" +pikchr-scale: "1.1" +timeline-arrowheads: 1 +timeline-circle-nodes: 1 +timeline-color-graph-lines: 1 +white-foreground: 0 + diff --git a/skins/darkmode/css.txt b/skins/darkmode/css.txt new file mode 100644 index 0000000..8a9d234 --- /dev/null +++ b/skins/darkmode/css.txt @@ -0,0 +1,603 @@ +/* General settings for the entire page */ +body { + margin: 0ex 1ex; + padding: 0; + background-color: #1f1f1f; + color: #ffffffe0; + font-family: sans-serif; +} + +/* The page title centered at the top of each page */ +div.title { + display: table-cell; + font-size: 2em; + font-weight: bold; + text-align: center; + vertical-align: bottom; + width: 100%; +} + +/* The login status message in the top right-hand corner */ +div.status { + display: table-cell; + text-align: right; + vertical-align: bottom; + color: #ddddddc9; + font-size: 0.8em; + font-weight: bold; + white-space: nowrap; +} +/* The leftoftitle is a
to the left of the title
+** that contains the same text as the status div. But we want +** the area to show as blank. The purpose is to cause the +** title to be exactly centered. */ +div.leftoftitle { + visibility: hidden; +} + +/* The header across the top of the page */ +header { + display: table; + width: 100%; +} + +/* The main menu bar that appears at the top of the page beneath +** the header */ +nav.mainmenu { + padding: 0.25em 0.5em; + font-size: 0.9em; + font-weight: bold; + text-align: center; + border-top-left-radius: 0.5em; + border-top-right-radius: 0.5em; + border-bottom: 1px dotted rgba(200,200,200,0.3); + z-index: 21; /* just above hbdrop */ +} +nav#hbdrop { + background-color: #1f1f1f; + border: 2px solid #303536; + border-radius: 0 0 0.5em 0.5em; + display: none; + left: 2em; + width: calc(100% - 4em); + position: absolute; + z-index: 20; /* just below mainmenu, but above timeline bubbles */ +} + +nav.mainmenu, div.submenu, div.sectionmenu { + color: #ffffffcc; + background-color: #303536/*#0000ff60*/; +} +/* The submenu bar that *sometimes* appears below the main menu */ +div.submenu, div.sectionmenu { + padding: 0.15em 0.5em 0.15em 0; + font-size: 0.9em; + text-align: center; + border-bottom-left-radius: 0.5em; + border-bottom-right-radius: 0.5em; +} +a, a:visited { + color: rgba(127, 201, 255, 0.9); + display: inline; + text-decoration: none; +} +a:visited {opacity: 0.8} +nav.mainmenu a, div.submenu a, +div.sectionmenu>a.button, div.submenu label, +footer a { + padding: 0.15em 0.5em; +} +nav.mainmenu a.active { + border-bottom: 1px solid #FF4500f0; +} +a:hover, +a:visited:hover { + background-color: #FF4500f0; + color: rgba(24,24,24,0.8); + border-radius: 0.1em; +} +.fileage tr:hover, +div.filetreeline:hover { + background-color: #333; +} +div.file-change-line button{ + background-color: #484848 +} +button, +input, +optgroup, +select, +textarea { + background: inherit; + color: inherit; + font: inherit; + margin: 0 +} +.button, +button, +input[type=button], +input[type=submit], +input[type=reset]{ + color: #ddd; + background-color: #446979; + border-radius: 5px; + border: 0; +} +.button:hover, +button:hover, +input[type=button]:hover, +input[type=submit]:hover, +input[type=reset]:hover { + background-color: #FF4500f0; + color: rgba(24,24,24,0.8); + outline: 0; +} +.button:disabled, +button:disabled, +input[type=button]:disabled, +input[type=submit]:disabled, +input[type=reset]:disabled{ + color: #222; + background-color: #b0b0b0; + opacity: 0.8; +} +.button:focus, +button:focus, +input[type=button]:focus, +input[type=submit]:focus, +input[type=reset]:focus { + outline: 2px outset #333; + border-color: #888; +} + +/* All page content from the bottom of the menu or submenu down to +** the footer */ +div.content { + padding: 0ex 1ex 1ex 1ex; +} + +/* Some pages have section dividers */ +div.section { + margin-bottom: 0; + margin-top: 1em; + padding: 0.1em; + font-size: 1.2em; + font-weight: bold; + background-color: #303536/*#0000ff60*/; + white-space: nowrap; + border-top-left-radius: 0.5em; + border-top-right-radius: 0.5em; + border-bottom: 1px dotted rgba(200,200,200,0.3); +} + +/* The "Date" that occurs on the left hand side of timelines */ +div.divider { + background: #303536; + border: 1px #558195 solid; + font-size: 1em; font-weight: normal; + padding: .25em; + margin: .2em 0 .2em 0; + float: left; + clear: left; + white-space: nowrap; +} + +/* The footer at the very bottom of the page */ +footer { + clear: both; + font-size: 0.8em; + padding: 0.15em 0.5em; + text-align: right; + background-color: #303536/*#0000ff60*/; + border-top: 1px dotted rgba(200,200,200,0.3); + border-bottom-left-radius: 0.5em; + border-bottom-right-radius: 0.5em; +} + +/* Hyperlink colors in the footer */ + +pre { + border-radius: 0.25em; +} +pre > code { + display: block; +} +/* verbatim blocks */ +pre.verbatim { + padding: 0.12em; + white-space: pre-wrap; +} +pre:not(.verbatim) { + margin-left: 1rem; + margin-right: 1rem; + background-color: rgba(200,200,200, 0.1); + padding: 0.5em 1em; +} + +/* The label/value pairs on (for example) the ci page */ +table.label-value th { + vertical-align: top; + text-align: right; + padding: 0.2ex 2ex; +} + +h1 {margin: 0.6em 0} +h2 {margin: 0.5em 0} +h3 {margin: 0.5em 0} +h4 {margin: 0.5em 0} +h5 {margin: 0.5em 0} + + +/********** +td.timelineTime, +tr.timelineBottom td { + border-bottom: 0 +} +table.timelineTable { + border-spacing: 0.3em 0.3em; +} +table.timelineTable tr td { + padding: 0.5em 1em; +} +.timelineModernCell[id], +.timelineColumnarCell[id], +.timelineDetailCell[id] { + background-color: #ffffff40; +} +table.timelineTable tr td:nth-of-type(2) { + background-color: #ffffffc0; +} +div.tl-canvas { +} +*/ + +.fossil-tooltip, +.fossil-toast-message { + background-color: rgba(251, 106, 0, 1); + border-color: rgba(127, 201, 255, 0.9); + color: black; +} + +/************************************************************************ +timeline... +************************************************************************/ +table.timelineTable tr:not(.timelineDateRow){ + background-color: #ffffff17; +} +table.timelineTable tr:not(.timelineDateRow):hover{ + background-color: #FF450080; +} +table.timelineTable tr td:first-of-type { + vertical-align: middle; + padding: 0.2em 0.5em; +} +div.timelineDate { + font-weight: 700; + white-space: nowrap; + border-radius: 0.2em; +} +td.timelineTime { + text-align: right; + white-space: nowrap; +} +td.timelineGraph { + width: 20px; + text-align: left; + border-bottom: 0 +} +a.timelineHistLink { + /*text-transform: lowercase*/ +} +span.timelineComment { + padding: 0 5px +} +.report th, +span.timelineEllipsis { + cursor: pointer +} +table.timelineTable { + border-spacing: 0 0.2em; +} +.timelineModernCell, .timelineColumnarCell, +.timelineDetailCell, .timelineCompactCell, +.timelineVerboseCell { + vertical-align: top; + text-align: left; + padding: .75em; + border-radius: 0.25em; + background: inherit /*#000*/; +} +.timelineSelected > .timelineColumnarCell, +.timelineSelected > .timelineCompactCell, +.timelineSelected > .timelineDetailCell, +.timelineSelected > .timelineModernCell, +.timelineSelected > .timelineVerboseCell { + padding: .75em; + border-radius: 0.2em; + border: 1px solid #ff8000; + vertical-align: top; + text-align: left; + background: #442800 +} + +/* Timeline has a blank line at the bottom. Apparently it's to provide the + graph with a good starting place. Hiding it causes a slight graph + unsightliness, but we can change its bg color. */ +table.timelineTable tr.timelineBottom, +table.timelineTable tr.timelineBottom:hover { + background: inherit; +} +span.timelineSelected { + border-radius: 0.2em; + border: 1px solid #ff8000; + /*vertical-align: top; + text-align: left;*/ + background: #442800 +} +.timelineSelected { + background-color: #ffffff40; +} +.timelineSecondary {} +.timelineSecondary > .timelineColumnarCell, +.timelineSecondary > .timelineCompactCell, +.timelineSecondary > .timelineDetailCell, +.timelineSecondary > .timelineModernCell, +.timelineSecondary > .timelineVerboseCell { + padding: .75em; + border-radius: 5px; + border: solid #0080ff; + /*vertical-align: top; + text-align: left;*/ + background: #002844 +} +span.timelineSecondary { + border-radius: 5px; + border: solid #0080ff; + /*vertical-align: top; + text-align: left;*/ + background: #002844 +} +.timelineCurrent > .timelineColumnarCell, +.timelineCurrent > .timelineCompactCell, +.timelineCurrent > .timelineDetailCell, +.timelineCurrent > .timelineModernCell, +.timelineCurrent > .timelineVerboseCell { + /*vertical-align: top; + text-align: left;*/ + padding: .75em; + border-radius: 5px; + border: dashed #ff8000 +} +.timelineModernCell[id], .timelineColumnarCell[id], .timelineDetailCell[id] { + background-color: inherit;/*#000*/ +} +.tl-canvas { + margin: 0 6px 0 10px +} +.tl-rail { + width: 18px +} +.tl-mergeoffset { + width: 2px +} +.tl-nodemark { + margin-top: .8em +} +.tl-node { + width: 10px; + height: 10px; + border: 2px solid #bbb; + background: #111; + cursor: pointer +} +.tl-node.leaf:after { + content: ''; + position: absolute; + top: 3px; + left: 3px; + width: 4px; + height: 4px; + background: #bbb +} +.tl-node.closed-leaf svg { + position: absolute; + top: 0px; + left: 0px; + width: 10px; + height: 10px; + color: #bbb; +} +.tl-node.sel:after { + content: ''; + position: absolute; + top: 1px; + left: 1px; + width: 8px; + height: 8px; + background: #ff8000 +} +.tl-arrow { + width: 0; + height: 0; + transform: scale(.999); + border: 0 solid transparent +} +.tl-arrow.u { + margin-top: -1px; + border-width: 0 3px; + border-bottom: 7px solid +} +.tl-arrow.u.sm { + border-bottom: 5px solid #bbb +} +.tl-line { + background: #bbb; + width: 2px +} +.tl-arrow.merge { + height: 1px; + border-width: 2px 0 +} +.tl-arrow.merge.l { + border-right: 3px solid #bbb +} +.tl-arrow.merge.r { + border-left: 3px solid #bbb +} +.tl-line.merge { + width: 1px +} +.tl-arrow.cherrypick { + height: 1px; + border-width: 2px 0; +} +.tl-arrow.cherrypick.l { + border-right: 3px solid #bbb; +} +.tl-arrow.cherrypick.r { + border-left: 3px solid #bbb; +} +.tl-line.cherrypick.h { + width: 0px; + border-top: 1px dashed #bbb; + border-left: 0px dashed #bbb; + background: rgba(255,255,255,0); +} +.tl-line.cherrypick.v { + width: 0px; + border-top: 0px dashed #bbb; + border-left: 1px dashed #bbb; + background: rgba(255,255,255,0); +} + +/************************************************************************ +diffs... +************************************************************************/ +td.diffln ins { + background-color: #559855; + color: #000; + text-decoration: none; +} +td.diffln del { + background-color: #c55; + color: #000; + text-decoration: none; +} +td.difftxt del { + background-color: inherit; + text-decoration: none; +} +td.difftxt del > del { + background-color: #c55; + color: #000; + text-decoration: none; +} +td.difftxt ins { + background-color: inherit; + text-decoration: none; +} +td.difftxt ins > ins { + background-color: #559855; + color: #000; + text-decoration: none; +} +tr.diffskip.jchunk { + background-color: black; +} +tr.diffskip > td.chunkctrl .jcbutton { + background-color: #303536; +} + +/************************************************************************ +************************************************************************/ +body.wikiedit #fossil-status-bar, +body.fileedit #fossil-status-bar{ + border-radius: 0.25em 0.25em 0 0; +} +.tab-container > .tabs { + border-radius: 0.25em; +} + +blockquote.file-content { + margin: 0; +} +blockquote.file-content > pre { + padding: 0; +} +blockquote.file-content > pre > code { + padding: 0 0.5em; +} +svg.pikchr { + /* swap the pikchr svg colors around so they're readable in + this dark theme. 2020-02: changes in fossil have made this + obsolete. */ + /*filter: invert(1) hue-rotate(180deg);*/ +} +span.snippet>mark { + color: white; + font-weight: bold; +} +input, textarea, select { + border: 1px solid rgba(127, 201, 255, 0.9); + padding: 1px; +} +select { + color: #1f1f1f; + background: #ffffffe0; +} +.capsumOff { + background-color: #222; +} +.capsumRead { + background-color: #262; +} +.capsumWrite { + background-color: #662; +} + +body.forum div.forumSel { + background: inherit; + border-left-width: 0.5em; + border-left-style: double; +} + +body.forum .debug { + background-color: #FF4500f0; + color: rgba(24,24,24,0.8); +} + +body.forum .forumPosts.fileage tr:hover { + background-color: #333; + color: rgba(24,24,24,0.8); +} +body.forum .forumPosts.fileage tr:hover { + background-color: #333; + color: rgba(24,24,24,0.8); +} +body.forum .forumPosts.fileage tr:hover > td:nth-child(1), +body.forum .forumPosts.fileage tr:hover > td:nth-child(3) { + color: #ffffffe0; +} + +body.forum .forumPostBody > div blockquote { + border: 1px inset; + padding: 0 0.5em; +} + +body.forum .forumPosts.fileage a:visited { + color: rgba(98, 150, 205, 0.9); +} + +body.report table.report tr td { color: black } +body.report table.report a { color: blue } +body.tkt td.tktDspValue { color: black } +body.tkt td.tktDspValue a { color: blue } + +body.branch .brlist > table > tbody > tr:hover:not(.selected), +body.branch .brlist > table > tbody > tr.selected { + background-color: #442800; +} + +p.noMoreShun { + color: #e5e500; +} diff --git a/skins/darkmode/details.txt b/skins/darkmode/details.txt new file mode 100644 index 0000000..03a63de --- /dev/null +++ b/skins/darkmode/details.txt @@ -0,0 +1,4 @@ +timeline-arrowheads: 0 +timeline-circle-nodes: 1 +timeline-color-graph-lines: 1 +white-foreground: 1 diff --git a/skins/darkmode/footer.txt b/skins/darkmode/footer.txt new file mode 100644 index 0000000..1714242 --- /dev/null +++ b/skins/darkmode/footer.txt @@ -0,0 +1,8 @@ + diff --git a/skins/darkmode/header.txt b/skins/darkmode/header.txt new file mode 100644 index 0000000..e95c4f9 --- /dev/null +++ b/skins/darkmode/header.txt @@ -0,0 +1,31 @@ +
+
+ if {[info exists login]} { + set logintext "$login\n" + } else { + set logintext "Login\n" + } + html $logintext +
+
$</div> + <div class="status"><nobr><th1> + html $logintext + </th1></nobr></div> +</header> +<nav class="mainmenu" title="Main Menu"> + <th1> + html "<a id='hbbtn' href='$home/sitemap' aria-label='Site Map'>☰</a>" + builtin_request_js hbmenu.js + foreach {name url expr class} $mainmenu { + if {![capexpr $expr]} continue + if {[string match /* $url]} { + if {[string match $url\[/?#\]* /$current_page/]} { + set class "active $class" + } + set url $home$url + } + html "<a href='$url' class='$class'>$name</a>\n" + } + </th1> +</nav> +<nav id="hbdrop" class='hbdrop' title="sitemap"></nav> diff --git a/skins/default/README.md b/skins/default/README.md new file mode 100644 index 0000000..57e0fea --- /dev/null +++ b/skins/default/README.md @@ -0,0 +1,10 @@ +This skin was originally contributed by Étienne Deparis on 2015-02-22, +promoted to the default on 2015-03-14, and subsequently changed by many: + + https://fossil-scm.org/home/finfo/skins/default/css.txt + https://fossil-scm.org/home/blame?filename=skins/default/css.txt&checkin=trunk + +In February 2024, a sufficiently large set of changes were made to the +skin that we forked the old version for the benefit of those who needed +to reference the old one — as when migrating custom skin changes to work +atop the new default — or who simply preferred it. See ../etienne. diff --git a/skins/default/css.txt b/skins/default/css.txt new file mode 100644 index 0000000..0269740 --- /dev/null +++ b/skins/default/css.txt @@ -0,0 +1,789 @@ +/* Overall page style; vi: filetype=css + */ + +body { + margin: 0 auto; + background-color: white; + font-family: sans-serif; +} + +a { + /* Unvisited links are a lightness-adjusted version of this skin's + * header blue, balancing contrast between the body text and the + * background in order to meet the goals specified by the WCAG 2 + * accessbility standard, earning us an "AA" grade according to + * the calculator result here: + * + * https://webaim.org/resources/linkcontrastchecker/?fcolor=2E2E2E&bcolor=FFFFFF&lcolor=3779BF + * + * It is for this same reason that our not-quite-black body text + * color is the shade of dark gray that it is. It can't be any + * lighter and still allow us to meet both targets. */ + color: #3779BF; + text-decoration: none; +} +a:hover { + color: #4183C4; + text-decoration: underline; +} + + +/* Page title, above menu bars */ + +.title { + color: #4183C4; + float: left; +} +h1.page-title { + font-size: 1.60em; /* match content > h1 */ + margin-bottom: 0; /* div.content top margin suffices */ + display: none; /* don't use body-area h1 except… */ +} +.artifact h1.page-title, +.dir h1.page-title, +.doc h1.page-title, +.wiki h1.page-title { + display: block; /* …for potentially long doc titles… */ + color: #444; +} +.artifact .title > .page-title, +.dir .title > .page-title, +.doc .title > .page-title, +.wiki .title > .page-title { + display: none; /* …where we suppress the title area h1 instead */ +} +.title h1 { + display: inline; + font-size: 2.20em; +} +.title h1:after { + content: " / "; + color: #777; + font-weight: normal; +} +.artifact .title h1:after, +.dir .title h1:after, +.doc .title h1:after, +.wiki .title h1:after { + content: ""; /* hide solidus for docs along with title h1 */ +} +.status { + float: right; + font-size: 0.8em; +} +div.logo { + float: left; + padding-right: 10px; +} +div.logo img { + max-height: 2em; /* smaller than title to keep it above the baseline */ +} + + +/* Main menu and optional sub-menu */ + +.mainmenu { + clear: both; + background: #eaeaea linear-gradient(#fafafa, #eaeaea) repeat-x; + border: 1px solid #eaeaea; + border-radius: 5px; + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + z-index: 21; /* just above hbdrop */ +} +.mainmenu a { + text-decoration: none; + color: #777; + border-right: 1px solid #eaeaea; +} +.mainmenu a.active, +.mainmenu a:hover { + color: #000; + border-bottom: 2px solid #D26911; +} +nav#hbdrop { + background-color: white; + border: 1px solid black; + border-top: white; + border-radius: 0 0 0.5em 0.5em; + display: none; + font-size: 80%; + left: 2em; + width: 90%; + padding-right: 1em; + position: absolute; + z-index: 20; /* just below mainmenu, but above timeline bubbles */ +} + +.submenu { + font-size: 0.8em; + padding: 10px; + border-bottom: 1px solid #ccc; +} +.submenu a, .submenu label { + padding: 10px 11px; + text-decoration: none; + color: #777; +} +.submenu label { + white-space: nowrap; +} +.submenu a:hover, .submenu label:hover { + padding: 6px 10px; + border: 1px solid #ccc; + border-radius: 5px; + color: #000; +} +span.submenuctrl, span.submenuctrl input, select.submenuctrl { + color: #777; +} +span.submenuctrl { + white-space: nowrap; +} + + +/* Main document area; elements common to most pages. */ + +.content { + padding: 1ex; + color: #2e2e2e; /* justified above in "WCAG 2" comment */ +} +.content h1 { font-size: 1.60em; color: #444; } +.content h2 { font-size: 1.45em; color: #444; } +.content h3 { font-size: 1.15em; color: #444; } +.content h4 { font-size: 1.05em; color: #444; } +.content h5 { font-size: 1.00em; color: #444; } + +.section { + font-size: 1em; + font-weight: bold; + background-color: #f5f5f5; + border: 1px solid #d8d8d8; + border-radius: 3px 3px 0 0; + padding: 9px 10px 10px; + margin: 10px 0; +} +.sectionmenu { + border: 1px solid #d8d8d8; + border-radius: 0 0 3px 3px; + border-top: 0; + margin-top: -10px; + margin-bottom: 10px; + padding: 10px; +} +.sectionmenu a { + display: inline-block; + margin-right: 1em; +} + +hr { + color: #eee; +} + + +/* Page footer */ + +footer { + border-top: 1px solid #ccc; + padding: 10px; + font-size: 0.8em; + margin-top: 10px; + color: #ccc; +} + +/* Forum */ + +.forum a:visited { + color: #6A7F94; +} + +div.forumSel { + animation: 1s linear 0s sel-fade; + background-color: white; /* animation end state */ + border-left: 4px solid black; /* after-animation selection indicator */ +} +@keyframes sel-fade { + from { background-color: #cef; } + to { background-color: white; } +} + +.forum form input { + margin: 0.5em 0; +} + + +/* Markdown and Wiki-formatted pages: /wiki, /doc, /file... */ + +.markdown blockquote, p.blockquote, .sidebar { + /* Override default.css version with our accent colors. Background is + * the solid version of rgba(65, 131, 196, 0.1) on white, needed to + * avoid tinting pre block backgrounds going "under" them. */ + background-color: #ebf2f9; + border-left-color: #4183c4; +} +div.sidebar { + /* Add extra whitespace between sidebar and content, both for spacing + * and to put a gap between it and any <pre> blocks that happen to run + * up against it. */ + outline: 1em solid white; +} + +/* Mark inline code fragments in the near-universal manner pioneered by + * Stack Overflow, then picked up by approximately everyone, including + * us, now. + * + * This combinatorial selector explosion results from a need to apply + * these stylings inside multiple page container types, multiplied by + * the surprisingly large number of tags HTML defines for semantically + * differentiated monospaced inline markup. If we do not target the + * elements we want to affect carefully, we'll end up overreaching, + * styling Fossil UI elements that use these tags for local purposes. + * + * HTML generated and emitted by Fossil UI does not always fall under + * the skin's generic rules; we must avoid intruding on its domain. + * Our limited intent here is to style user content only, where it is + * unreasonable to expect its author to take the time to hand-craft + * per-document styling. Contrast Fossil UI, which often does exactly + * that in order to get particular results. + * + * Its rough equivalent in Sass syntax is far more compact, thus clearer: + * + * .artifact, .dir, .doc, .forum, .wiki // the page types we target + * > .content // hands off header & footer + * &, > .fossil-doc, > .markdown // wiki, HTML & MD emb docs + * > p // in top-level paras only + * > code, > kbd, > samp, > tt, > var // monospaced tag types + * background-color: #f4f4f4 // pale gray box which… + * padding: 0 4px // …extends around the sides + * + * We then need something similar for the block-level pre elements. + * + * The CSS below is based on feeding that Sass code through this: + * + * $ sassc code.sass | sed -e 's/, /,\n/g' + * + * …then hand-cleansing it to make it _somewhat_ more understandable. + * That largely amounts to whitespace tweaks, but we've also done things + * like trim back the forum-specific styling to apply to the default MD + * markup only; direct HTML formatting isn't even an option there, and + * while wiki markup _is_ supported, MD was the default from day 1. + * Another quirk of the forum post handling is that the .markdown class + * gets applied per-post, not up at the top level as with the wiki, + * embedded docs, etc. + */ +.artifact > .content > p > code, +.artifact > .content > p > kbd, +.artifact > .content > p > samp, +.artifact > .content > p > tt, +.artifact > .content > p > var, +.artifact > .content > .fossil-doc > p > code, +.artifact > .content > .fossil-doc > p > kbd, +.artifact > .content > .fossil-doc > p > samp, +.artifact > .content > .fossil-doc > p > tt, +.artifact > .content > .fossil-doc > p > var, +.artifact > .content > .markdown > p > code, +.artifact > .content > .markdown > p > kbd, +.artifact > .content > .markdown > p > samp, +.artifact > .content > .markdown > p > tt, +.artifact > .content > .markdown > p > var, +.dir > .content > p > code, +.dir > .content > p > kbd, +.dir > .content > p > samp, +.dir > .content > p > tt, +.dir > .content > p > var, +.dir > .content > .fossil-doc > p > code, +.dir > .content > .fossil-doc > p > kbd, +.dir > .content > .fossil-doc > p > samp, +.dir > .content > .fossil-doc > p > tt, +.dir > .content > .fossil-doc > p > var, +.dir > .content > .markdown > p > code, +.dir > .content > .markdown > p > kbd, +.dir > .content > .markdown > p > samp, +.dir > .content > .markdown > p > tt, +.dir > .content > .markdown > p > var, +.doc > .content > p > code, +.doc > .content > p > kbd, +.doc > .content > p > samp, +.doc > .content > p > tt, +.doc > .content > p > var, +.doc > .content > .fossil-doc > p > code, +.doc > .content > .fossil-doc > p > kbd, +.doc > .content > .fossil-doc > p > samp, +.doc > .content > .fossil-doc > p > tt, +.doc > .content > .fossil-doc > p > var, +.doc > .content > .markdown > p > code, +.doc > .content > .markdown > p > kbd, +.doc > .content > .markdown > p > samp, +.doc > .content > .markdown > p > tt, +.doc > .content > .markdown > p > var, +.forum > .content .markdown > p > code, +.forum > .content .markdown > p > kbd, +.forum > .content .markdown > p > samp, +.forum > .content .markdown > p > tt, +.forum > .content .markdown > p > var, +.wiki > .content > p > code, +.wiki > .content > p > kbd, +.wiki > .content > p > samp, +.wiki > .content > p > tt, +.wiki > .content > p > var, +.wiki > .content > .fossil-doc > p > code, +.wiki > .content > .fossil-doc > p > kbd, +.wiki > .content > .fossil-doc > p > samp, +.wiki > .content > .fossil-doc > p > tt, +.wiki > .content > .fossil-doc > p > var, +.wiki > .content > .markdown > p > code, +.wiki > .content > .markdown > p > kbd, +.wiki > .content > .markdown > p > samp, +.wiki > .content > .markdown > p > tt, +.wiki > .content > .markdown > p > var, +.artifact > .content > pre, +.artifact > .content > .fossil-doc > pre, +.artifact > .content > .markdown > pre, +.dir > .content > pre, +.dir > .content > .fossil-doc > pre, +.dir > .content > .markdown > pre, +.doc > .content > pre, +.doc > .content > .fossil-doc > pre, +.doc > .content > .markdown > pre, +.forum > .content .markdown > pre, +.wiki > .content > pre, +.wiki > .content > .fossil-doc > pre, +.wiki > .content > .markdown > pre { + background-color: #f4f4f4; + padding: 0 4px; +} +.content pre, table.numbered-lines > tbody > tr { + hyphens: none; + line-height: 1.25; +} + +.content ul:not(.browser) > li { + list-style-type: disc; +} + +div.filetree ul li.dir, +div.filetree ul li.subdir, +div.filetree ul li.file{ + list-style-type: none; +} + +.artifact > .content table, +.dir > .content table, +.doc > .content table, +.wiki > .content table { + background-color: #f0f5f9; + border: 1px solid #a7c2dc; + border-radius: 0.5em; + border-spacing: 0; + padding: 6px; +} +.artifact > .content th, +.dir > .content th, +.doc > .content th, +.wiki > .content th { + border-bottom: 1px solid #dee8f2; + padding-bottom: 4px; + padding-right: 6px; +} +.artifact > .content tr > th, +.dir > .content tr > th, +.doc > .content tr > th, +.wiki > .content tr > th { + background-color: #dee8f0; +} +.artifact > .content tr:nth-child(odd), +.dir > .content tr:nth-child(odd), +.doc > .content tr:nth-child(odd), +.wiki > .content tr:nth-child(odd) { + background-color: #e0e8ee; +} +.artifact > .content td, +.dir > .content td, +.doc > .content td, +.wiki > .content td { + padding-bottom: 4px; + padding-right: 6px; +} +th { + /* Special rule at high level to override default centering of table + header cell text. If it isn't at this level, it can't be + overridden in the HTML, as by the MD table generator's handling + of `:` alignment markers. */ + text-align: left; +} + +/* Wiki adjustments */ +pre.verbatim { + /* keep code examples from crashing into sidebars, etc. */ + white-space: pre-wrap; +} +textarea.wikiedit { + /* Monospace fonts tend to have smaller x-heights; compensate. + * Can't do this generally because not all fonts have this problem. + * A textarea stands alone, whereas inline <code> has to work with + * the browser's choice of sans-serif proportional font. */ + font-size: 1.1em; +} + + +/* Tickets */ + +table.report { + cursor: auto; + border: 1px solid #ccc; + border-radius: 0.5em; + margin: 1em 0; +} +.report td, .report th { + border: 0; + font-size: .8em; + padding: 10px; +} +.report td:first-child { + border-top-left-radius: 5px; +} +.report tbody tr:last-child td:first-child { + border-bottom-left-radius: 5px; +} +.report td:last-child { + border-top-right-radius: 5px; +} +.report tbody tr:last-child { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; +} +.report tbody tr:last-child td:last-child { + border-bottom-right-radius: 5px; +} +.report th { + cursor: pointer; +} +.report thead+tbody tr:hover { + background-color: #f5f9fc !important; +} + +td.tktDspLabel { + width: 70px; + text-align: right; + overflow: hidden; +} +td.tktDspValue { + text-align: left; + vertical-align: top; + background-color: #f8f8f8; + border: 1px solid #ccc; +} +td.tktDspValue pre { + white-space: pre-wrap; +} + + +/* Timeline */ + +span.timelineDetail { + font-size: 90%; +} +div.timelineDate { + font-weight: bold; + white-space: nowrap; +} + +/* Extend default.css comment cell rounding to the whole row for the + * various types of "selected" rows, making them "hang" into the left + * margin, distinguishing them from the coloring used for branch cells. + * Care must be taken to avoid having the box-shadow rounded but the + * background squared-off. */ +table.timelineTable { + padding: 0 3px; /* leave space to sides for box shadow; can clip otherwise */ +} +table.timelineTable tr { + border-radius: 1em; +} +tr.timelineSelected, tr.timelineSecondary { background-color: unset; } +tr.timelineSelected td, span.timelineSelected { + background-color: #fbe8d5; +} +tr.timelineSecondary td, span.timelineSecondary { + background-color: #d5e8fb; +} +tr.timelineCurrent td:first-child, +tr.timelineSecondary td:first-child, +tr.timelineSelected td:first-child { + border-top-left-radius: 1em; + border-bottom-left-radius: 1em; +} +tr.timelineCurrent td:last-child, +tr.timelineSecondary td:last-child, +tr.timelineSelected td:last-child { + border-top-right-radius: 1em; + border-bottom-right-radius: 1em; +} +tr.timelineCurrent td { + border-top: 1px dashed #446979; + border-bottom: 1px dashed #446979; +} +tr.timelineCurrent td:first-child { + border-left: 1px dashed #446979; +} +tr.timelineCurrent td:last-child { + border-right: 1px dashed #446979; +} + + +/* Miscellaneous UI elements */ + +.fossil-tooltip.help-buttonlet-content { + background-color: lightyellow; +} + + +/* Exceptions for specific screen sizes */ + +@media screen and (max-width: 600px) { + /* Spacing for mobile */ + body { + padding-left: 4px; + padding-right: 4px; + } + .content { + font-size: 0.9em; + } + .title { + padding-top: 0px; + padding-bottom: 0px; + } + .title > .page-title { + display: inline; /* show page titles above menu bar… */ + } + .artifact .title > .page-title, + .dir .title > .page-title, + .doc .title > .page-title, + .wiki .title > .page-title { + display: none; /* …except for docs, where it may force wrapping */ + } + .status {padding-top: 0px;} + .mainmenu a { + padding: 8px 10px; + } + .mainmenu { + padding: 10px; + } +} +@media screen and (min-width: 600px) { + /* Spacing for desktop */ + body { + padding-left: 20px; + padding-right: 20px; + } + .title { + padding-top: 10px; + padding-bottom: 10px; + } + span.page-title { + font-size: 18px; + } + div.logo { + padding-top: 10px; + } + .status {padding-top: 30px;} + .mainmenu a { + padding: 8px 20px; + } + .mainmenu { + padding: 10px; + } + + /* Wide screens mean long lines. Add extra leading to give the eye a + * "gutter" to follow from the end of one to the start of the next. */ + .content dd, + .content dt, + .content div, + .content li, + .content p, + .content table { + line-height: 1.4em; + } + + /* This horror show has the same cause that informed our handling of + * <code> and friends above; see "combinatorial selector explosion." + * Without this careful targeting, we'd not only overreach into areas + * of Fossil UI where our meddling is not wanted, we would mistakenly + * apply double indents to nested formatting in MD forum posts, p + * within td tags, and more. + * + * Rather than give the equivalent Sass code here, see the SCSS file + * that the [Inskinerator](https://tangentsoft.com/inskinerator/) + * project ships as override/modern/media.scss. Rendering that + * through sassc gives substantially identical output, modulo the + * hand-polishing we've done here. */ + .artifact > .content > p, + .artifact > .content > .markdown > p, + .artifact > .content > .fossil-doc > p, + .artifact > .content > ol, .artifact > .content > ul, + .artifact > .content > .markdown > ol, .artifact > .content > .markdown > ul, + .artifact > .content > .fossil-doc > ol, .artifact > .content > .fossil-doc > ul, + .artifact > .content > table, + .artifact > .content > .markdown > table, + .artifact > .content > .fossil-doc > table, + .dir > .content > p, + .dir > .content > .markdown > p, + .dir > .content > .fossil-doc > p, + .dir > .content > ol, .dir > .content > ul, + .dir > .content > .markdown > ol, .dir > .content > .markdown > ul, + .dir > .content > .fossil-doc > ol, .dir > .content > .fossil-doc > ul, + .dir > .content > table, + .dir > .content > .markdown > table, + .dir > .content > .fossil-doc > table, + .doc > .content > p, + .doc > .content > .markdown > p, + .doc > .content > .fossil-doc > p, + .doc > .content > ol, .doc > .content > ul, + .doc > .content > .markdown > ol, .doc > .content > .markdown > ul, + .doc > .content > .fossil-doc > ol, .doc > .content > .fossil-doc > ul, + .doc > .content > table, + .doc > .content > .markdown > table, + .doc > .content > .fossil-doc > table, + .wiki > .content > p, + .wiki > .content > .markdown > p, + .wiki > .content > .fossil-doc > p, + .wiki > .content > ol, .wiki > .content > ul, + .wiki > .content > .markdown > ol, .wiki > .content > .markdown > ul, + .wiki > .content > .fossil-doc > ol, .wiki > .content > .fossil-doc > ul, + .wiki > .content > table, + .wiki > .content > .markdown > table, + .wiki > .content > .fossil-doc > table, + #fileedit-tab-preview-wrapper > p, + #fileedit-tab-preview-wrapper > ol, + #fileedit-tab-preview-wrapper > ul, + #fileedit-tab-preview-wrapper > table, + #fileedit-tab-preview-wrapper > .markdown > p, + #fileedit-tab-preview-wrapper > .markdown > ol, + #fileedit-tab-preview-wrapper > .markdown > ul, + #fileedit-tab-preview-wrapper > .markdown > table, + #wikiedit-tab-preview-wrapper > p, + #wikiedit-tab-preview-wrapper > ol, + #wikiedit-tab-preview-wrapper > ul, + #wikiedit-tab-preview-wrapper > table, + #wikiedit-tab-preview-wrapper > .markdown > p, + #wikiedit-tab-preview-wrapper > .markdown > ol, + #wikiedit-tab-preview-wrapper > .markdown > ul, + #wikiedit-tab-preview-wrapper > .markdown > table { + margin-left: 50pt; + margin-right: 50pt; + } + + /* Code blocks get extra indenting. We need a selector explosion + * equally powerful to the one above for inline <code> fragments and + * similar elements, for essentially the same reason: Fossil UI also + * uses <pre>, and we want to affect user content only. + * + * The equivalent Sass code is: + * + * .artifact, .dir, .doc, .wiki // doc types we target + * > .content // hands off header & footer + * @import 'pre-doc-margins.sass' + * + * #fileedit-tab-preview-wrapper, // include /fileedit previews + * #wikiedit-tab-preview-wrapper // ditto /wikiedit + * @import 'pre-doc-margins.sass' + * + * …where pre-doc-margins.sass contains the elements common to both: + * + * &, > .fossil-doc, > .markdown // wiki, HTML & MD doc types + * > pre // direct pre descendants only + * margin-left: 70pt; + * margin-right: 50pt; + * + * This is a technical overreach since /wiki & /wikiedit lack support + * for Fossil's HTML embedded doc markup capability, but we prefer to + * draw the /fileedit parallel in our Sass example over the dubious + * pleasure of being nit-picky on this point. Instead, we've chosen + * to back that overreach out by hand below. + */ + .artifact > .content > pre, + .artifact > .content > .fossil-doc > pre, + .artifact > .content > .markdown > pre, + .dir > .content > pre, + .dir > .content > .fossil-doc > pre, + .dir > .content > .markdown > pre, + .doc > .content > pre, + .doc > .content > .fossil-doc > pre, + .doc > .content > .markdown > pre, + .wiki > .content > pre, + .wiki > .content > .markdown > pre { + margin-left: 70pt; + margin-right: 50pt; + } + #fileedit-tab-preview-wrapper > pre, + #wikiedit-tab-preview-wrapper > pre, + #fileedit-tab-preview-wrapper > .fossil-doc > pre, + #fileedit-tab-preview-wrapper > .markdown > pre, + #wikiedit-tab-preview-wrapper > .markdown > pre { + margin-left: 70pt; + margin-right: 50pt; + } + .forum > .content .markdown > pre { + margin-left: 20pt; /* special case for MD in forum; need less indent */ + } + + /* Fossil UI uses these, but in sufficiently constrained ways that we + * don't have to be nearly as careful to avoid an overreach. */ + .doc > .content h1, .artifact .content h1, .dir .content h1, .fileedit .content h1, .wiki .content h1 { margin-left: 10pt; } + .doc > .content h2, .artifact .content h2, .dir .content h2, .fileedit .content h2, .wiki .content h2 { margin-left: 20pt; } + .doc > .content h3, .artifact .content h3, .dir .content h3, .fileedit .content h3, .wiki .content h3 { margin-left: 30pt; } + .doc > .content h4, .artifact .content h4, .dir .content h4, .fileedit .content h4, .wiki .content h4 { margin-left: 40pt; } + .doc > .content h5, .artifact .content h5, .dir .content h5, .fileedit .content h5, .wiki .content h5 { margin-left: 50pt; } + .doc > .content hr, .artifact .content hr, .dir .content hr, .fileedit .content hr, .wiki .content hr { margin-left: 10pt; } + + /* Don't need to be nearly as careful with tags Fossil UI doesn't use. */ + .doc dd, .artifact dd, .dir dd, .fileedit dd, .wikiedit dd { margin-left: 30pt; margin-bottom: 1em; } + .doc dl, .artifact dl, .dir dl, .fileedit dl, .wikiedit dl { margin-left: 60pt; } + .doc dt, .artifact dt, .dir dt, .fileedit dt, .wikiedit dt { margin-left: 10pt; } + + /* Fossil UI doesn't use Pikchr at all (yet?) so we can be quite loose + * with these selectors. */ + .content .pikchr-wrapper { margin-left: 70pt; } + div.pikchr-wrapper.indent:not(.source) { + /* Selector naming scheme mismatch is intentional: it must match the + * way it's given in default.css exactly if it is to override it. */ + margin-left: 70pt; + margin-right: 50pt; + } + div.pikchr-wrapper.center:not(.source), + div.pikchr-wrapper.float-right:not(.source) { + margin-left: 0; + } + + /* Special treatment for backward compatibility. */ + .indent, /* clean alternative to misusing <blockquote> */ + .artifact > .content > blockquote:not(.file-content), + .dir > .content > blockquote, + .doc > .content > blockquote, + .fileedit > .content > blockquote, + .wiki > .content > blockquote { + /* We must apply extra indent relative to "p" since Fossil's wiki + * generator misuses the blockquote tag against HTML and MD norms + * to mean "indented paragraph." Skip it for file content retrieved + * by /dir URLs. */ + margin-left: 80pt; + } + .artifact > .content > .markdown > blockquote, + .dir > .content > .markdown > blockquote, + .doc > .content > .markdown > blockquote, + .fileedit > .content > .markdown > blockquote, + .wiki > .content > .markdown > blockquote { + /* Fossil MD didn't inherit that bug; its HTML generator emits + * blockquote tags only for _block quotes_! A moderate indent + * suffices due to the visual styling applied above. */ + margin-left: 60pt; + } + + /* Alternative to BLOCK.indent when wrapped in something that is + * itself indented. The value is the delta between p and blockquote + * above, expressed as padding instead of margin so it adds to the + * outer margin instead of forcing the browser into picking one. */ + .local-indent { + padding-left: 30pt; + } +} diff --git a/skins/default/details.txt b/skins/default/details.txt new file mode 100644 index 0000000..581045d --- /dev/null +++ b/skins/default/details.txt @@ -0,0 +1,6 @@ +pikchr-fontscale: "0.9" +pikchr-scale: "1.1" +timeline-arrowheads: 1 +timeline-circle-nodes: 1 +timeline-color-graph-lines: 1 +white-foreground: 0 diff --git a/skins/default/footer.txt b/skins/default/footer.txt new file mode 100644 index 0000000..7d201d6 --- /dev/null +++ b/skins/default/footer.txt @@ -0,0 +1,5 @@ +<footer> +This page was generated in about +<th1>puts [expr {([utime]+[stime]+1000)/1000*0.001}]</th1>s by +Fossil $release_version $manifest_version $manifest_date +</footer> diff --git a/skins/default/header.txt b/skins/default/header.txt new file mode 100644 index 0000000..583e7bb --- /dev/null +++ b/skins/default/header.txt @@ -0,0 +1,67 @@ +<header> + <div class="logo"> + <th1> + ## See skins/original/header.txt for commentary; not repeated here. + proc getLogoUrl { baseurl } { + set idx(first) [string first // $baseurl] + if {$idx(first) != -1} { + set idx(first+1) [expr {$idx(first) + 2}] + set idx(nextRange) [string range $baseurl $idx(first+1) end] + set idx(next) [string first / $idx(nextRange)] + if {$idx(next) != -1} { + set idx(next) [expr {$idx(next) + $idx(first+1)}] + set idx(next-1) [expr {$idx(next) - 1}] + set scheme [string range $baseurl 0 $idx(first)] + set host [string range $baseurl $idx(first+1) $idx(next-1)] + if {[string compare $scheme http:/] == 0} { + set scheme http:// + } else { + set scheme https:// + } + set logourl $scheme$host/ + } else { + set logourl $baseurl + } + } else { + set logourl $baseurl + } + return $logourl + } + set logourl [getLogoUrl $baseurl] + </th1> + <a href="$logourl"> + <img src="$logo_image_url" border="0" alt="$<project_name>"> + </a> + </div> + <div class="title"> + <h1>$<project_name></h1> + <span class="page-title">$<title></span> + </div> + <div class="status"> + <th1> + if {[info exists login]} { + html "<a href='$home/login'>$login</a>\n" + } else { + html "<a href='$home/login'>Login</a>\n" + } + </th1> + </div> +</header> +<nav class="mainmenu" title="Main Menu"> + <th1> + html "<a id='hbbtn' href='$home/sitemap' aria-label='Site Map'>☰</a>" + builtin_request_js hbmenu.js + foreach {name url expr class} $mainmenu { + if {![capexpr $expr]} continue + if {[string match /* $url]} { + if {[string match $url\[/?#\]* /$current_page/]} { + set class "active $class" + } + set url $home$url + } + html "<a href='$url' class='$class'>$name</a>\n" + } + </th1> +</nav> +<nav id="hbdrop" class='hbdrop' title="sitemap"></nav> +<h1 class="page-title">$<title></h1> diff --git a/skins/eagle/details.txt b/skins/eagle/details.txt new file mode 100644 index 0000000..14eac9c --- /dev/null +++ b/skins/eagle/details.txt @@ -0,0 +1,5 @@ +timeline-arrowheads: 1 +timeline-circle-nodes: 0 +timeline-color-graph-lines: 0 +white-foreground: 1 +pikchr-background: 0x485d7b diff --git a/skins/etienne/details.txt b/skins/etienne/details.txt new file mode 100644 index 0000000..a7f228d --- /dev/null +++ b/skins/etienne/details.txt @@ -0,0 +1,4 @@ +timeline-arrowheads: 1 +timeline-circle-nodes: 1 +timeline-color-graph-lines: 1 +white-foreground: 0 diff --git a/skins/khaki/details.txt b/skins/khaki/details.txt new file mode 100644 index 0000000..a7f228d --- /dev/null +++ b/skins/khaki/details.txt @@ -0,0 +1,4 @@ +timeline-arrowheads: 1 +timeline-circle-nodes: 1 +timeline-color-graph-lines: 1 +white-foreground: 0 diff --git a/skins/original/details.txt b/skins/original/details.txt new file mode 100644 index 0000000..3e05845 --- /dev/null +++ b/skins/original/details.txt @@ -0,0 +1,4 @@ +timeline-arrowheads: 1 +timeline-circle-nodes: 0 +timeline-color-graph-lines: 0 +white-foreground: 0 diff --git a/skins/plain_gray/details.txt b/skins/plain_gray/details.txt new file mode 100644 index 0000000..07f1951 --- /dev/null +++ b/skins/plain_gray/details.txt @@ -0,0 +1,4 @@ +timeline-arrowheads: 1 +timeline-circle-nodes: 1 +timeline-color-graph-lines: 0 +white-foreground: 0 diff --git a/skins/xekri/details.txt b/skins/xekri/details.txt new file mode 100644 index 0000000..d77ed8e --- /dev/null +++ b/skins/xekri/details.txt @@ -0,0 +1,4 @@ +timeline-arrowheads: 1 +timeline-circle-nodes: 0 +timeline-color-graph-lines: 1 +white-foreground: 1 diff --git a/src/db.c b/src/db.c new file mode 100644 index 0000000..43f6081 --- /dev/null +++ b/src/db.c @@ -0,0 +1,5733 @@ +/* +** Copyright (c) 2006 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** Code for interfacing to the various databases. +** +** There are three separate database files that fossil interacts +** with: +** +** (1) The "configdb" database in ~/.fossil or ~/.config/fossil.db +** or in %LOCALAPPDATA%/_fossil +** +** (2) The "repository" database +** +** (3) A local check-out database named "_FOSSIL_" or ".fslckout" +** and located at the root of the local copy of the source tree. +** +*/ +#include "config.h" +#if defined(_WIN32) +# if USE_SEE +# include <windows.h> +# define GETPID (int)GetCurrentProcessId +# endif +#else +# include <pwd.h> +# if USE_SEE +# define GETPID getpid +# endif +#endif +#if USE_SEE && !defined(SQLITE_HAS_CODEC) +# define SQLITE_HAS_CODEC +#endif +#if USE_SEE && defined(__linux__) +# include <sys/uio.h> +#endif +#include <sqlite3.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <time.h> + +/* BUGBUG: This (PID_T) does not work inside of INTERFACE block. */ +#if USE_SEE +#if defined(_WIN32) +typedef DWORD PID_T; +#else +typedef pid_t PID_T; +#endif +#endif + +#include "db.h" + +#if INTERFACE +/* +** Type definitions used for handling the saved encryption key for SEE. +*/ +#if !defined(_WIN32) +typedef void *LPVOID; +typedef size_t SIZE_T; +#endif + +/* +** Operations for db_maybe_handle_saved_encryption_key_for_process, et al. +*/ +#define SEE_KEY_READ ((int)0) +#define SEE_KEY_WRITE ((int)1) +#define SEE_KEY_ZERO ((int)2) + +/* +** An single SQL statement is represented as an instance of the following +** structure. +*/ +struct Stmt { + Blob sql; /* The SQL for this statement */ + sqlite3_stmt *pStmt; /* The results of sqlite3_prepare_v2() */ + Stmt *pNext, *pPrev; /* List of all unfinalized statements */ + int nStep; /* Number of sqlite3_step() calls */ + int rc; /* Error from db_vprepare() */ +}; + +/* +** Copy this to initialize a Stmt object to a clean/empty state. This +** is useful to help avoid assertions when performing cleanup in some +** error handling cases. +*/ +#define empty_Stmt_m {BLOB_INITIALIZER,NULL, NULL, NULL, 0, 0} +#endif /* INTERFACE */ +const struct Stmt empty_Stmt = empty_Stmt_m; + +/* +** Call this routine when a database error occurs. +** This routine throws a fatal error. It does not return. +*/ +static void db_err(const char *zFormat, ...){ + va_list ap; + char *z; + va_start(ap, zFormat); + z = vmprintf(zFormat, ap); + va_end(ap); +#ifdef FOSSIL_ENABLE_JSON + if( g.json.isJsonMode!=0 ){ + /* + ** Avoid calling into the JSON support subsystem if it + ** has not yet been initialized, e.g. early SQLite log + ** messages, etc. + */ + json_bootstrap_early(); + json_err( 0, z, 1 ); + } + else +#endif /* FOSSIL_ENABLE_JSON */ + if( g.xferPanic && g.cgiOutput==1 ){ + cgi_reset_content(); + @ error Database\serror:\s%F(z) + cgi_reply(); + } + if( strstr(z,"attempt to write a readonly database") ){ + static const char *azDbNames[] = { "repository", "localdb", "configdb" }; + int i; + for(i=0; i<3; i++){ + if( sqlite3_db_readonly(g.db, azDbNames[i])==1 ){ + z = mprintf("\"%s\" is readonly.\n%s", + sqlite3_db_filename(g.db,azDbNames[i]), z); + } + } + } + fossil_fatal("Database error: %s", z); +} + +/* +** Check a result code. If it is not SQLITE_OK, print the +** corresponding error message and exit. +*/ +static void db_check_result(int rc, Stmt *pStmt){ + if( rc!=SQLITE_OK ){ + db_err("SQL error (%d,%d: %s) while running [%s]", + rc, sqlite3_extended_errcode(g.db), + sqlite3_errmsg(g.db), blob_str(&pStmt->sql)); + } +} + +/* +** All static variable that a used by only this file are gathered into +** the following structure. +*/ +static struct DbLocalData { + unsigned protectMask; /* Prevent changes to database */ + int nBegin; /* Nesting depth of BEGIN */ + int doRollback; /* True to force a rollback */ + int nCommitHook; /* Number of commit hooks */ + int wrTxn; /* Outer-most TNX is a write */ + Stmt *pAllStmt; /* List of all unfinalized statements */ + int nPrepare; /* Number of calls to sqlite3_prepare_v2() */ + int nDeleteOnFail; /* Number of entries in azDeleteOnFail[] */ + struct sCommitHook { + int (*xHook)(void); /* Functions to call at db_end_transaction() */ + int sequence; /* Call functions in sequence order */ + } aHook[6]; + char *azDeleteOnFail[3]; /* Files to delete on a failure */ + char *azBeforeCommit[5]; /* Commands to run prior to COMMIT */ + int nBeforeCommit; /* Number of entries in azBeforeCommit */ + int nPriorChanges; /* sqlite3_total_changes() at transaction start */ + const char *zStartFile; /* File in which transaction was started */ + int iStartLine; /* Line of zStartFile where transaction started */ + int (*xAuth)(void*,int,const char*,const char*,const char*,const char*); + void *pAuthArg; /* Argument to the authorizer */ + const char *zAuthName; /* Name of the authorizer */ + int bProtectTriggers; /* True if protection triggers already exist */ + int nProtect; /* Slots of aProtect used */ + unsigned aProtect[12]; /* Saved values of protectMask */ + int pauseDmlLog; /* Ignore pDmlLog if positive */ + Blob *pDmlLog; /* Append DML statements here, of not NULL */ +} db = { + PROTECT_USER|PROTECT_CONFIG|PROTECT_BASELINE, /* protectMask */ + 0, 0, 0, 0, 0, 0, 0, {{0}}, {0}, {0}, 0, 0, 0, 0, 0, 0, 0, 0, 0, {0}}; + +/* +** Arrange for the given file to be deleted on a failure. +*/ +void db_delete_on_failure(const char *zFilename){ + assert( db.nDeleteOnFail<count(db.azDeleteOnFail) ); + if( zFilename==0 ) return; + db.azDeleteOnFail[db.nDeleteOnFail++] = fossil_strdup(zFilename); +} + +/* +** Return the transaction nesting depth. 0 means we are currently +** not in a transaction. +*/ +int db_transaction_nesting_depth(void){ + return db.nBegin; +} + +/* +** Return a pointer to a string that is the code point where the +** current transaction was started. +*/ +char *db_transaction_start_point(void){ + return mprintf("%s:%d", db.zStartFile, db.iStartLine); +} + +/* +** This routine is called by the SQLite commit-hook mechanism +** just prior to each commit. All this routine does is verify +** that nBegin really is zero. That insures that transactions +** cannot commit by any means other than by calling db_end_transaction() +** below. +** +** This is just a safety and sanity check. +*/ +static int db_verify_at_commit(void *notUsed){ + if( db.nBegin ){ + fossil_panic("illegal commit attempt"); + return 1; + } + return 0; +} + +/* +** Silently add the filename and line number as parameter to each +** db_begin_transaction call. +*/ +#if INTERFACE +#define db_begin_transaction() db_begin_transaction_real(__FILE__,__LINE__) +#define db_begin_write() db_begin_write_real(__FILE__,__LINE__) +#define db_commit_transaction() db_end_transaction(0) +#define db_rollback_transaction() db_end_transaction(1) +#endif + +/* +** Begin a nested transaction +*/ +void db_begin_transaction_real(const char *zStartFile, int iStartLine){ + if( db.nBegin==0 ){ + db_multi_exec("BEGIN"); + sqlite3_commit_hook(g.db, db_verify_at_commit, 0); + db.nPriorChanges = sqlite3_total_changes(g.db); + db.doRollback = 0; + db.zStartFile = zStartFile; + db.iStartLine = iStartLine; + db.wrTxn = 0; + } + db.nBegin++; +} +/* +** Begin a new transaction for writing. +*/ +void db_begin_write_real(const char *zStartFile, int iStartLine){ + if( db.nBegin==0 ){ + if( !db_is_writeable("repository") ){ + db_multi_exec("BEGIN"); + }else{ + db_multi_exec("BEGIN IMMEDIATE"); + sqlite3_commit_hook(g.db, db_verify_at_commit, 0); + db.nPriorChanges = sqlite3_total_changes(g.db); + db.doRollback = 0; + db.zStartFile = zStartFile; + db.iStartLine = iStartLine; + db.wrTxn = 1; + } + }else if( !db.wrTxn ){ + fossil_warning("read txn at %s:%d might cause SQLITE_BUSY " + "for the write txn at %s:%d", + db.zStartFile, db.iStartLine, zStartFile, iStartLine); + } + db.nBegin++; +} + +/* End a transaction previously started using db_begin_transaction() +** or db_begin_write(). +*/ +void db_end_transaction(int rollbackFlag){ + if( g.db==0 ) return; + if( db.nBegin<=0 ){ + fossil_warning("Extra call to db_end_transaction"); + return; + } + if( rollbackFlag ){ + db.doRollback = 1; + if( g.fSqlTrace ) fossil_trace("-- ROLLBACK by request\n"); + } + db.nBegin--; + if( db.nBegin==0 ){ + int i; + if( db.doRollback==0 && db.nPriorChanges<sqlite3_total_changes(g.db) ){ + i = 0; + db_protect_only(PROTECT_SENSITIVE); + while( db.nBeforeCommit ){ + db.nBeforeCommit--; + sqlite3_exec(g.db, db.azBeforeCommit[i], 0, 0, 0); + sqlite3_free(db.azBeforeCommit[i]); + i++; + } + leaf_do_pending_checks(); + db_protect_pop(); + } + for(i=0; db.doRollback==0 && i<db.nCommitHook; i++){ + int rc = db.aHook[i].xHook(); + if( rc ){ + db.doRollback = 1; + if( g.fSqlTrace ) fossil_trace("-- ROLLBACK due to aHook[%d]\n", i); + } + } + while( db.pAllStmt ){ + db_finalize(db.pAllStmt); + } + db_multi_exec("%s", db.doRollback ? "ROLLBACK" : "COMMIT"); + db.doRollback = 0; + } +} + +/* +** Force a rollback and shutdown the database +*/ +void db_force_rollback(void){ + int i; + static int busy = 0; + sqlite3_stmt *pStmt = 0; + if( busy || g.db==0 ) return; + busy = 1; + undo_rollback(); + while( (pStmt = sqlite3_next_stmt(g.db,pStmt))!=0 ){ + sqlite3_reset(pStmt); + } + while( db.pAllStmt ){ + db_finalize(db.pAllStmt); + } + if( db.nBegin ){ + sqlite3_exec(g.db, "ROLLBACK", 0, 0, 0); + db.nBegin = 0; + } + busy = 0; + db_close(0); + for(i=0; i<db.nDeleteOnFail; i++){ + file_delete(db.azDeleteOnFail[i]); + } +} + +/* +** Install a commit hook. Hooks are installed in sequence order. +** It is an error to install the same commit hook more than once. +** +** Each commit hook is called (in order of ascending sequence) at +** each commit operation. If any commit hook returns non-zero, +** the subsequence commit hooks are omitted and the transaction +** rolls back rather than commit. It is the responsibility of the +** hooks themselves to issue any error messages. +*/ +void db_commit_hook(int (*x)(void), int sequence){ + int i; + assert( db.nCommitHook < count(db.aHook) ); + for(i=0; i<db.nCommitHook; i++){ + assert( x!=db.aHook[i].xHook ); + if( db.aHook[i].sequence>sequence ){ + int s = sequence; + int (*xS)(void) = x; + sequence = db.aHook[i].sequence; + x = db.aHook[i].xHook; + db.aHook[i].sequence = s; + db.aHook[i].xHook = xS; + } + } + db.aHook[db.nCommitHook].sequence = sequence; + db.aHook[db.nCommitHook].xHook = x; + db.nCommitHook++; +} + +#if INTERFACE +/* +** Flag bits for db_protect() and db_unprotect() indicating which parts +** of the databases should be write protected or write enabled, respectively. +*/ +#define PROTECT_USER 0x01 /* USER table */ +#define PROTECT_CONFIG 0x02 /* CONFIG and GLOBAL_CONFIG tables */ +#define PROTECT_SENSITIVE 0x04 /* Sensitive and/or global settings */ +#define PROTECT_READONLY 0x08 /* everything except TEMP tables */ +#define PROTECT_BASELINE 0x10 /* protection system is working */ +#define PROTECT_ALL 0x1f /* All of the above */ +#define PROTECT_NONE 0x00 /* Nothing. Everything is open */ +#endif /* INTERFACE */ + +/* +** Enable or disable database write protections. +** +** db_protext(X) Add protects on X +** db_unprotect(X) Remove protections on X +** db_protect_only(X) Remove all prior protections then set +** protections to only X. +** +** Each of these routines pushes the previous protection mask onto +** a finite-size stack. Each should be followed by a call to +** db_protect_pop() to pop the stack and restore the protections that +** existed prior to the call. The protection mask stack has a limited +** depth, so take care not to nest calls too deeply. +** +** About Database Write Protection +** ------------------------------- +** +** This is *not* a primary means of defending the application from +** attack. Fossil should be secure even if this mechanism is disabled. +** The purpose of database write protection is to provide an additional +** layer of defense in case SQL injection bugs somehow slip into other +** parts of the system. In other words, database write protection is +** not the primary defense but rather defense in depth. +** +** This mechanism mostly focuses on the USER table, to prevent an +** attacker from giving themselves Admin privilegs, and on the +** CONFIG table and especially "sensitive" settings such as +** "diff-command" or "editor" that if compromised by an attacker +** could lead to an RCE. +** +** By default, the USER and CONFIG tables are read-only. Various +** subsystems that legitimately need to change those tables can +** temporarily do so using: +** +** db_unprotect(PROTECT_xxx); +** // make the legitmate changes here +** db_protect_pop(); +** +** Code that runs inside of reduced protections should be carefully +** reviewed to ensure that it is harmless and not subject to SQL +** injection. +** +** Read-only operations (such as many web pages like /timeline) +** can invoke db_protect(PROTECT_ALL) to effectively make the database +** read-only. TEMP tables (which are often used for these kinds of +** pages) are still writable, however. +** +** The PROTECT_SENSITIVE protection is a subset of PROTECT_CONFIG +** that blocks changes to all of the global_config table, but only +** "sensitive" settings in the config table. PROTECT_SENSITIVE +** relies on triggers and the protected_setting() SQL function to +** prevent changes to sensitive settings. +** +** PROTECT_READONLY is set for any HTTP request for which the HTTP_REFERER +** is not the same origin. This is an additional defense against cross-site- +** scripting attacks. As with all of these defenses, this is only an extra +** backup layer. Fossil should be proof against XSS attacks even without this. +** +** Any violation of these security restrictions results in a SECURITY message +** in the server log (if enabled). A violation of any of these restrictions +** probably indicates a bug in Fossil and should be reported to the +** developers. +** +** Additional Notes +** ---------------- +** +** Calls to routines like db_set() and db_unset() temporarily disable +** the PROTECT_CONFIG protection. The assumption is that these calls +** cannot be invoked by an SQL injection and are thus safe. Make sure +** this is the case by always using a string literal as the name argument +** to db_set() and db_unset() and friend, not a variable that might +** be compromised by an attack. +*/ +void db_protect_only(unsigned flags){ + if( db.nProtect>=count(db.aProtect)-2 ){ + fossil_panic("too many db_protect() calls"); + } + db.aProtect[db.nProtect++] = db.protectMask; + if( (flags & PROTECT_SENSITIVE)!=0 + && db.bProtectTriggers==0 + && g.repositoryOpen + ){ + /* Create the triggers needed to protect sensitive settings from + ** being created or modified the first time that PROTECT_SENSITIVE + ** is enabled. Deleting a sensitive setting is harmless, so there + ** is not trigger to block deletes. After being created once, the + ** triggers persist for the life of the database connection. */ + unsigned savedProtectMask = db.protectMask; + db.protectMask = 0; + db_multi_exec( + "CREATE TEMP TRIGGER protect_1 BEFORE INSERT ON config" + " WHEN protected_setting(new.name) BEGIN" + " SELECT raise(abort,'not authorized');" + "END;\n" + "CREATE TEMP TRIGGER protect_2 BEFORE UPDATE ON config" + " WHEN protected_setting(new.name) BEGIN" + " SELECT raise(abort,'not authorized');" + "END;\n" + ); + db.bProtectTriggers = 1; + db.protectMask = savedProtectMask; + } + db.protectMask = flags; +} +void db_protect(unsigned flags){ + db_protect_only(db.protectMask | flags); +} +void db_unprotect(unsigned flags){ + if( db.nProtect>=count(db.aProtect)-2 ){ + fossil_panic("too many db_unprotect() calls"); + } + db.aProtect[db.nProtect++] = db.protectMask; + db.protectMask &= ~(flags|PROTECT_READONLY); +} +void db_protect_pop(void){ + if( db.nProtect<1 ){ + fossil_panic("too many db_protect_pop() calls"); + } + db.protectMask = db.aProtect[--db.nProtect]; +} +int db_is_protected(unsigned flags){ + return (db.protectMask & flags)!=0; +} + +/* +** Verify that the desired database write protections are in place. +** Throw a fatal error if not. +*/ +void db_assert_protected(unsigned flags){ + if( (flags & db.protectMask)!=flags ){ + fossil_fatal("missing database write protection bits: %02x", + flags & ~db.protectMask); + } +} + +/* +** Assert that either all protections are off (including PROTECT_BASELINE +** which is usually always enabled), or the setting named in the argument +** is no a sensitive setting. +** +** This assert() is used to verify that the db_set() and db_set_int() +** interfaces do not modify a sensitive setting. +*/ +void db_assert_protection_off_or_not_sensitive(const char *zName){ + if( db.protectMask!=0 && db_setting_is_protected(zName) ){ + fossil_panic("unauthorized change to protected setting \"%s\"", zName); + } +} + +/* +** Every Fossil database connection automatically registers the following +** overarching authenticator callback, and leaves it registered for the +** duration of the connection. This authenticator will call any +** sub-authenticators that are registered using db_set_authorizer(). +** +** == Testing Notes == +** +** Run Fossil as using a command like this: +** +** ./fossil sql --test --errorlog - +** +** Then enter SQL commands like one of these: +** +** SELECT db_protect('user'); +** SELECT db_protect('config'); +** SELECT db_protect('sensitive'); +** SELECT db_protect('readonly'); +** SELECT db_protect('all'); +** +** Then try to do SQL statements that would violate the constraints and +** verify that SECURITY warnings appear in the error log output. See +** also the sqlcmd_db_protect() function in sqlcmd.c. +*/ +int db_top_authorizer( + void *pNotUsed, + int eCode, + const char *z0, + const char *z1, + const char *z2, + const char *z3 +){ + int rc = SQLITE_OK; + switch( eCode ){ + case SQLITE_INSERT: + case SQLITE_UPDATE: + case SQLITE_DELETE: { + if( (db.protectMask & PROTECT_USER)!=0 + && sqlite3_stricmp(z0,"user")==0 ){ + fossil_errorlog( + "SECURITY: authorizer blocks DML on protected USER table\n"); + rc = SQLITE_DENY; + }else if( (db.protectMask & PROTECT_CONFIG)!=0 && + (sqlite3_stricmp(z0,"config")==0 || + sqlite3_stricmp(z0,"global_config")==0) ){ + fossil_errorlog( + "SECURITY: authorizer blocks DML on protected table \"%s\"\n", z0); + rc = SQLITE_DENY; + }else if( (db.protectMask & PROTECT_SENSITIVE)!=0 && + sqlite3_stricmp(z0,"global_config")==0 ){ + fossil_errorlog( + "SECURITY: authorizer blocks DML on protected GLOBAL_CONFIG table\n"); + rc = SQLITE_DENY; + }else if( (db.protectMask & PROTECT_READONLY)!=0 + && (sqlite3_stricmp(z2, "repository")==0 + || sqlite3_stricmp(z2,"configdb")==0 + || sqlite3_stricmp(z2,"localdb")==0) ){ + /* The READONLY constraint only applies to persistent database files. + ** "temp" and "mem1" and other transient databases are not + ** constrained by READONLY. */ + fossil_errorlog( + "SECURITY: authorizer blocks DML on table \"%s\" due to the " + "request coming from a different origin\n", z0); + rc = SQLITE_DENY; + } + break; + } + case SQLITE_DROP_TEMP_TRIGGER: { + /* Do not allow the triggers that enforce PROTECT_SENSITIVE + ** to be dropped */ + fossil_errorlog( + "SECURITY: authorizer blocks attempt to drop a temporary trigger\n"); + rc = SQLITE_DENY; + break; + } + } + if( db.xAuth && rc==SQLITE_OK ){ + rc = db.xAuth(db.pAuthArg, eCode, z0, z1, z2, z3); + } + return rc; +} + +/* +** Set or unset the query authorizer callback function +*/ +void db_set_authorizer( + int(*xAuth)(void*,int,const char*,const char*,const char*,const char*), + void *pArg, + const char *zName /* for tracing */ +){ + if( db.xAuth ){ + fossil_panic("multiple active db_set_authorizer() calls"); + } + db.xAuth = xAuth; + db.pAuthArg = pArg; + db.zAuthName = zName; + if( g.fSqlTrace ) fossil_trace("-- set authorizer %s\n", zName); +} +void db_clear_authorizer(void){ + if( db.zAuthName && g.fSqlTrace ){ + fossil_trace("-- discontinue authorizer %s\n", db.zAuthName); + } + db.xAuth = 0; + db.pAuthArg = 0; + db.zAuthName = 0; +} + +#if INTERFACE +/* +** Possible flags to db_vprepare +*/ +#define DB_PREPARE_IGNORE_ERROR 0x001 /* Suppress errors */ +#define DB_PREPARE_PERSISTENT 0x002 /* Stmt will stick around for a while */ +#endif + +/* +** If zSql is a DML statement, append it db.pDmlLog. +*/ +static void db_append_dml(const char *zSql){ + size_t nSql; + if( db.pDmlLog==0 ) return; + if( db.pauseDmlLog ) return; + if( zSql==0 ) return; + nSql = strlen(zSql); + while( nSql>0 && fossil_isspace(zSql[0]) ){ nSql--; zSql++; } + while( nSql>0 && fossil_isspace(zSql[nSql-1]) ) nSql--; + if( nSql<6 ) return; + if( fossil_strnicmp(zSql, "SELECT", 6)==0 ) return; + if( fossil_strnicmp(zSql, "PRAGMA", 6)==0 ) return; + blob_append(db.pDmlLog, zSql, nSql); + if( zSql[nSql-1]!=';' ) blob_append_char(db.pDmlLog, ';'); + blob_append_char(db.pDmlLog, '\n'); +} + +/* +** Set the Blob to which DML statement text should be appended. Set it +** to zero to stop appending DML statement text. +*/ +void db_append_dml_to_blob(Blob *pBlob){ + db.pDmlLog = pBlob; +} + +/* +** Pause or unpause the DML log +*/ +void db_pause_dml_log(void){ db.pauseDmlLog++; } +void db_unpause_dml_log(void){ db.pauseDmlLog--; } + +/* +** Prepare a Stmt. Assume that the Stmt is previously uninitialized. +** If the input string contains multiple SQL statements, only the first +** one is processed. All statements beyond the first are silently ignored. +*/ +int db_vprepare(Stmt *pStmt, int flags, const char *zFormat, va_list ap){ + int rc; + int prepFlags = 0; + char *zSql; + const char *zExtra = 0; + blob_zero(&pStmt->sql); + blob_vappendf(&pStmt->sql, zFormat, ap); + va_end(ap); + zSql = blob_str(&pStmt->sql); + db.nPrepare++; + db_append_dml(zSql); + if( flags & DB_PREPARE_PERSISTENT ){ + prepFlags = SQLITE_PREPARE_PERSISTENT; + } + rc = sqlite3_prepare_v3(g.db, zSql, -1, prepFlags, &pStmt->pStmt, &zExtra); + if( rc!=0 && (flags & DB_PREPARE_IGNORE_ERROR)==0 ){ + db_err("%s\n%s", sqlite3_errmsg(g.db), zSql); + }else if( zExtra && !fossil_all_whitespace(zExtra) ){ + db_err("surplus text follows SQL: \"%s\"", zExtra); + } + pStmt->pNext = db.pAllStmt; + pStmt->pPrev = 0; + if( db.pAllStmt ) db.pAllStmt->pPrev = pStmt; + db.pAllStmt = pStmt; + pStmt->nStep = 0; + pStmt->rc = rc; + return rc; +} +int db_prepare(Stmt *pStmt, const char *zFormat, ...){ + int rc; + va_list ap; + va_start(ap, zFormat); + rc = db_vprepare(pStmt, 0, zFormat, ap); + va_end(ap); + return rc; +} +int db_prepare_ignore_error(Stmt *pStmt, const char *zFormat, ...){ + int rc; + va_list ap; + va_start(ap, zFormat); + rc = db_vprepare(pStmt, DB_PREPARE_IGNORE_ERROR, zFormat, ap); + va_end(ap); + return rc; +} + +/* This variant of db_prepare() checks to see if the statement has +** already been prepared, and if it has it becomes a no-op. +*/ +int db_static_prepare(Stmt *pStmt, const char *zFormat, ...){ + int rc = SQLITE_OK; + if( blob_size(&pStmt->sql)==0 ){ + va_list ap; + va_start(ap, zFormat); + rc = db_vprepare(pStmt, DB_PREPARE_PERSISTENT, zFormat, ap); + va_end(ap); + } + return rc; +} + +/* Return TRUE if static Stmt object pStmt has been initialized. +*/ +int db_static_stmt_is_init(Stmt *pStmt){ + return blob_size(&pStmt->sql)>0; +} + +/* Prepare a statement using text placed inside a Blob +** using blob_append_sql(). +*/ +int db_prepare_blob(Stmt *pStmt, Blob *pSql){ + int rc; + char *zSql; + pStmt->sql = *pSql; + blob_init(pSql, 0, 0); + zSql = blob_sql_text(&pStmt->sql); + db.nPrepare++; + rc = sqlite3_prepare_v3(g.db, zSql, -1, 0, &pStmt->pStmt, 0); + if( rc!=0 ){ + db_err("%s\n%s", sqlite3_errmsg(g.db), zSql); + } + pStmt->pNext = pStmt->pPrev = 0; + pStmt->nStep = 0; + pStmt->rc = rc; + return rc; +} + + +/* +** Return the index of a bind parameter +*/ +static int paramIdx(Stmt *pStmt, const char *zParamName){ + int i = sqlite3_bind_parameter_index(pStmt->pStmt, zParamName); + if( i==0 ){ + db_err("no such bind parameter: %s\nSQL: %b", zParamName, &pStmt->sql); + } + return i; +} +/* +** Bind an integer, string, or Blob value to a named parameter. +*/ +int db_bind_int(Stmt *pStmt, const char *zParamName, int iValue){ + return sqlite3_bind_int(pStmt->pStmt, paramIdx(pStmt, zParamName), iValue); +} +int db_bind_int64(Stmt *pStmt, const char *zParamName, i64 iValue){ + return sqlite3_bind_int64(pStmt->pStmt, paramIdx(pStmt, zParamName), iValue); +} +int db_bind_double(Stmt *pStmt, const char *zParamName, double rValue){ + return sqlite3_bind_double(pStmt->pStmt, paramIdx(pStmt, zParamName), rValue); +} +int db_bind_text(Stmt *pStmt, const char *zParamName, const char *zValue){ + return sqlite3_bind_text(pStmt->pStmt, paramIdx(pStmt, zParamName), zValue, + -1, SQLITE_STATIC); +} +int db_bind_text16(Stmt *pStmt, const char *zParamName, const char *zValue){ + return sqlite3_bind_text16(pStmt->pStmt, paramIdx(pStmt, zParamName), zValue, + -1, SQLITE_STATIC); +} +int db_bind_null(Stmt *pStmt, const char *zParamName){ + return sqlite3_bind_null(pStmt->pStmt, paramIdx(pStmt, zParamName)); +} +int db_bind_blob(Stmt *pStmt, const char *zParamName, Blob *pBlob){ + return sqlite3_bind_blob(pStmt->pStmt, paramIdx(pStmt, zParamName), + blob_buffer(pBlob), blob_size(pBlob), SQLITE_STATIC); +} + +/* bind_str() treats a Blob object like a TEXT string and binds it +** to the SQL variable. Contrast this to bind_blob() which treats +** the Blob object like an SQL BLOB. +*/ +int db_bind_str(Stmt *pStmt, const char *zParamName, Blob *pBlob){ + return sqlite3_bind_text(pStmt->pStmt, paramIdx(pStmt, zParamName), + blob_buffer(pBlob), blob_size(pBlob), SQLITE_STATIC); +} + +/* +** Step the SQL statement. Return either SQLITE_ROW or an error code +** or SQLITE_OK if the statement finishes successfully. +*/ +int db_step(Stmt *pStmt){ + int rc; + if( pStmt->pStmt==0 ) return pStmt->rc; + rc = sqlite3_step(pStmt->pStmt); + pStmt->nStep++; + return rc; +} + +/* +** Print warnings if a query is inefficient. +*/ +static void db_stats(Stmt *pStmt){ +#ifdef FOSSIL_DEBUG + int c1, c2, c3; + const char *zSql = sqlite3_sql(pStmt->pStmt); + if( zSql==0 ) return; + c1 = sqlite3_stmt_status(pStmt->pStmt, SQLITE_STMTSTATUS_FULLSCAN_STEP, 1); + c2 = sqlite3_stmt_status(pStmt->pStmt, SQLITE_STMTSTATUS_AUTOINDEX, 1); + c3 = sqlite3_stmt_status(pStmt->pStmt, SQLITE_STMTSTATUS_SORT, 1); + if( c1>pStmt->nStep*4 && strstr(zSql,"/*scan*/")==0 ){ + fossil_warning("%d scan steps for %d rows in [%s]", c1, pStmt->nStep, zSql); + }else if( c2 ){ + fossil_warning("%d automatic index rows in [%s]", c2, zSql); + }else if( c3 && strstr(zSql,"/*sort*/")==0 && strstr(zSql,"/*scan*/")==0 ){ + fossil_warning("sort w/o index in [%s]", zSql); + } + pStmt->nStep = 0; +#endif +} + +/* +** Reset or finalize a statement. +*/ +int db_reset(Stmt *pStmt){ + int rc; + if( g.fSqlStats ){ db_stats(pStmt); } + rc = sqlite3_reset(pStmt->pStmt); + db_check_result(rc, pStmt); + return rc; +} +int db_finalize(Stmt *pStmt){ + int rc; + if( pStmt->pNext ){ + pStmt->pNext->pPrev = pStmt->pPrev; + } + if( pStmt->pPrev ){ + pStmt->pPrev->pNext = pStmt->pNext; + }else if( db.pAllStmt==pStmt ){ + db.pAllStmt = pStmt->pNext; + } + pStmt->pNext = 0; + pStmt->pPrev = 0; + if( g.fSqlStats ){ db_stats(pStmt); } + blob_reset(&pStmt->sql); + rc = sqlite3_finalize(pStmt->pStmt); + db_check_result(rc, pStmt); + pStmt->pStmt = 0; + return rc; +} + +/* +** Return the rowid of the most recent insert +*/ +int db_last_insert_rowid(void){ + i64 x = sqlite3_last_insert_rowid(g.db); + if( x<0 || x>(i64)2147483647 ){ + fossil_panic("rowid out of range (0..2147483647)"); + } + return (int)x; +} + +/* +** Return the number of rows that were changed by the most recent +** INSERT, UPDATE, or DELETE. Auxiliary changes caused by triggers +** or other side effects are not counted. +*/ +int db_changes(void){ + return sqlite3_changes(g.db); +} + +/* +** Extract text, integer, or blob values from the N-th column of the +** current row. +*/ +int db_column_type(Stmt *pStmt, int N){ + return sqlite3_column_type(pStmt->pStmt, N); +} +int db_column_bytes(Stmt *pStmt, int N){ + return sqlite3_column_bytes(pStmt->pStmt, N); +} +int db_column_int(Stmt *pStmt, int N){ + return sqlite3_column_int(pStmt->pStmt, N); +} +i64 db_column_int64(Stmt *pStmt, int N){ + return sqlite3_column_int64(pStmt->pStmt, N); +} +double db_column_double(Stmt *pStmt, int N){ + return sqlite3_column_double(pStmt->pStmt, N); +} +const char *db_column_text(Stmt *pStmt, int N){ + return (char*)sqlite3_column_text(pStmt->pStmt, N); +} +const char *db_column_raw(Stmt *pStmt, int N){ + return (const char*)sqlite3_column_blob(pStmt->pStmt, N); +} +const char *db_column_name(Stmt *pStmt, int N){ + return (char*)sqlite3_column_name(pStmt->pStmt, N); +} +int db_column_count(Stmt *pStmt){ + return sqlite3_column_count(pStmt->pStmt); +} +char *db_column_malloc(Stmt *pStmt, int N){ + return fossil_strdup_nn(db_column_text(pStmt, N)); +} +void db_column_blob(Stmt *pStmt, int N, Blob *pBlob){ + blob_append(pBlob, sqlite3_column_blob(pStmt->pStmt, N), + sqlite3_column_bytes(pStmt->pStmt, N)); +} +Blob db_column_text_as_blob(Stmt *pStmt, int N){ + Blob x; + blob_init(&x, (char*)sqlite3_column_text(pStmt->pStmt,N), + sqlite3_column_bytes(pStmt->pStmt,N)); + return x; +} + +/* +** Initialize a blob to an ephemeral copy of the content of a +** column in the current row. The data in the blob will become +** invalid when the statement is stepped or reset. +*/ +void db_ephemeral_blob(Stmt *pStmt, int N, Blob *pBlob){ + blob_init(pBlob, sqlite3_column_blob(pStmt->pStmt, N), + sqlite3_column_bytes(pStmt->pStmt, N)); +} + +/* +** Execute a single prepared statement until it finishes. +*/ +int db_exec(Stmt *pStmt){ + int rc; + while( (rc = db_step(pStmt))==SQLITE_ROW ){} + rc = db_reset(pStmt); + db_check_result(rc, pStmt); + return rc; +} + +/* +** COMMAND: test-db-exec-error +** Usage: %fossil test-db-exec-error +** +** Invoke the db_exec() interface with an erroneous SQL statement +** in order to verify the error handling logic. +*/ +void db_test_db_exec_cmd(void){ + Stmt err; + db_find_and_open_repository(0,0); + db_prepare(&err, "INSERT INTO repository.config(name) VALUES(NULL);"); + db_exec(&err); +} + +/* +** COMMAND: test-db-prepare +** Usage: %fossil test-db-prepare ?OPTIONS? SQL-STATEMENT +** +** Options: +** --auth-report Enable the ticket report query authorizer +** --auth-ticket Enable the ticket schema query authorizer +** +** Invoke db_prepare() on the SQL input. Report any errors encountered. +** This command is used to verify error detection logic in the db_prepare() +** utility routine. +*/ +void db_test_db_prepare(void){ + const int fAuthReport = find_option("auth-report",0,0)!=0; + const int fAuthSchema = find_option("auth-ticket",0,0)!=0; + char * zReportErr = 0; /* auth-report error string. */ + int nSchemaErr = 0; /* Number of auth-ticket errors. */ + Stmt err; + + if(fAuthReport + fAuthSchema > 1){ + fossil_fatal("Only one of --auth-report or --auth-ticket " + "may be used."); + } + db_find_and_open_repository(0,0); + verify_all_options(); + if( g.argc!=3 ) usage("?OPTIONS? SQL"); + if(fAuthReport){ + report_restrict_sql(&zReportErr); + }else if(fAuthSchema){ + ticket_restrict_sql(&nSchemaErr); + } + db_prepare(&err, "%s", g.argv[2]/*safe-for-%s*/); + db_finalize(&err); + if(fAuthReport){ + report_unrestrict_sql(); + if(zReportErr){ + fossil_warning("Report authorizer error: %s\n", zReportErr); + fossil_free(zReportErr); + } + }else if(fAuthSchema){ + ticket_unrestrict_sql(); + if(nSchemaErr){ + fossil_warning("Ticket schema authorizer error count: %d\n", + nSchemaErr); + } + } +} + +/* +** Print the output of one or more SQL queries on standard output. +** This routine is used for debugging purposes only. +*/ +int db_debug(const char *zSql, ...){ + Blob sql; + int rc = SQLITE_OK; + va_list ap; + const char *z, *zEnd; + sqlite3_stmt *pStmt; + blob_init(&sql, 0, 0); + va_start(ap, zSql); + blob_vappendf(&sql, zSql, ap); + va_end(ap); + z = blob_str(&sql); + while( rc==SQLITE_OK && z[0] ){ + pStmt = 0; + rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd); + if( rc!=SQLITE_OK ) break; + if( pStmt ){ + int nRow = 0; + db.nPrepare++; + while( sqlite3_step(pStmt)==SQLITE_ROW ){ + int i, n; + if( nRow++ > 0 ) fossil_print("\n"); + n = sqlite3_column_count(pStmt); + for(i=0; i<n; i++){ + fossil_print("%s = %s\n", sqlite3_column_name(pStmt, i), + sqlite3_column_text(pStmt,i)); + } + } + rc = sqlite3_finalize(pStmt); + if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z); + } + z = zEnd; + } + blob_reset(&sql); + return rc; +} + +/* +** Execute multiple SQL statements. The input text is executed +** directly without any formatting. +*/ +int db_exec_sql(const char *z){ + int rc = SQLITE_OK; + sqlite3_stmt *pStmt; + const char *zEnd; + while( rc==SQLITE_OK && z[0] ){ + pStmt = 0; + rc = sqlite3_prepare_v2(g.db, z, -1, &pStmt, &zEnd); + if( rc ){ + db_err("%s: {%s}", sqlite3_errmsg(g.db), z); + }else if( pStmt ){ + db.nPrepare++; + db_append_dml(sqlite3_sql(pStmt)); + while( sqlite3_step(pStmt)==SQLITE_ROW ){} + rc = sqlite3_finalize(pStmt); + if( rc ) db_err("%s: {%.*s}", sqlite3_errmsg(g.db), (int)(zEnd-z), z); + } + z = zEnd; + } + return rc; +} + +/* +** Execute multiple SQL statements using printf-style formatting. +*/ +int db_multi_exec(const char *zSql, ...){ + Blob sql; + int rc; + va_list ap; + + blob_init(&sql, 0, 0); + va_start(ap, zSql); + blob_vappendf(&sql, zSql, ap); + va_end(ap); + rc = db_exec_sql(blob_str(&sql)); + blob_reset(&sql); + return rc; +} + +/* +** Optionally make the following changes to the database if feasible and +** convenient. Do not start a transaction for these changes, but only +** make these changes if other changes are also being made. +*/ +void db_optional_sql(const char *zDb, const char *zSql, ...){ + if( db_is_writeable(zDb) && db.nBeforeCommit < count(db.azBeforeCommit) ){ + va_list ap; + va_start(ap, zSql); + db.azBeforeCommit[db.nBeforeCommit++] = sqlite3_vmprintf(zSql, ap); + va_end(ap); + } +} + +/* +** Execute a query and return a single integer value. +*/ +i64 db_int64(i64 iDflt, const char *zSql, ...){ + va_list ap; + Stmt s; + i64 rc; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)!=SQLITE_ROW ){ + rc = iDflt; + }else{ + rc = db_column_int64(&s, 0); + } + db_finalize(&s); + return rc; +} +int db_int(int iDflt, const char *zSql, ...){ + va_list ap; + Stmt s; + int rc; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)!=SQLITE_ROW ){ + rc = iDflt; + }else{ + rc = db_column_int(&s, 0); + } + db_finalize(&s); + return rc; +} + +/* +** Return TRUE if the query would return 1 or more rows. Return +** FALSE if the query result would be an empty set. +*/ +int db_exists(const char *zSql, ...){ + va_list ap; + Stmt s; + int rc; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)!=SQLITE_ROW ){ + rc = 0; + }else{ + rc = 1; + } + db_finalize(&s); + return rc; +} + + +/* +** Execute a query and return a floating-point value. +*/ +double db_double(double rDflt, const char *zSql, ...){ + va_list ap; + Stmt s; + double r; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)!=SQLITE_ROW ){ + r = rDflt; + }else{ + r = db_column_double(&s, 0); + } + db_finalize(&s); + return r; +} + +/* +** Execute a query and append the first column of the first row +** of the result set to blob given in the first argument. +*/ +void db_blob(Blob *pResult, const char *zSql, ...){ + va_list ap; + Stmt s; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)==SQLITE_ROW ){ + blob_append(pResult, sqlite3_column_blob(s.pStmt, 0), + sqlite3_column_bytes(s.pStmt, 0)); + } + db_finalize(&s); +} + +/* +** Execute a query. Return the first column of the first row +** of the result set as a string. Space to hold the string is +** obtained from fossil_strdup() and should be freed using fossil_free(). +** If the result set is empty, return a copy of zDefault instead. +*/ +char *db_text(const char *zDefault, const char *zSql, ...){ + va_list ap; + Stmt s; + char *z; + va_start(ap, zSql); + db_vprepare(&s, 0, zSql, ap); + va_end(ap); + if( db_step(&s)==SQLITE_ROW ){ + z = fossil_strdup_nn((const char*)sqlite3_column_text(s.pStmt, 0)); + }else{ + z = fossil_strdup(zDefault); + } + db_finalize(&s); + return z; +} + +/* +** Initialize a new database file with the given schema. If anything +** goes wrong, call db_err() to exit. +** +** If zFilename is NULL, then create an empty repository in an in-memory +** database. +*/ +void db_init_database( + const char *zFileName, /* Name of database file to create */ + const char *zSchema, /* First part of schema */ + ... /* Additional SQL to run. Terminate with NULL. */ +){ + sqlite3 *xdb; + int rc; + const char *zSql; + va_list ap; + + xdb = db_open(zFileName ? zFileName : ":memory:"); + sqlite3_exec(xdb, "BEGIN EXCLUSIVE", 0, 0, 0); + rc = sqlite3_exec(xdb, zSchema, 0, 0, 0); + if( rc!=SQLITE_OK ){ + db_err("%s", sqlite3_errmsg(xdb)); + } + va_start(ap, zSchema); + while( (zSql = va_arg(ap, const char*))!=0 ){ + rc = sqlite3_exec(xdb, zSql, 0, 0, 0); + if( rc!=SQLITE_OK ){ + db_err("%s", sqlite3_errmsg(xdb)); + } + } + va_end(ap); + sqlite3_exec(xdb, "COMMIT", 0, 0, 0); + if( zFileName || g.db!=0 ){ + sqlite3_close(xdb); + }else{ + g.db = xdb; + } +} + +/* +** Function to return the number of seconds since 1970. This is +** the same as strftime('%s','now') but is more compact. +*/ +void db_now_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + sqlite3_result_int64(context, time(0)); +} + +/* +** Function to return the check-in time for a file. +** +** checkin_mtime(CKINID,RID) +** +** CKINID: The RID for the manifest for a check-in. +** RID: The RID of a file in CKINID for which the check-in time +** is desired. +** +** Returns: The check-in time in seconds since 1970. +*/ +void db_checkin_mtime_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + i64 mtime; + int rc = mtime_of_manifest_file(sqlite3_value_int(argv[0]), + sqlite3_value_int(argv[1]), &mtime); + if( rc==0 ){ + sqlite3_result_int64(context, mtime); + } +} + +/* +** SQL wrapper around the symbolic_name_to_rid() C-language API. +** Examples: +** +** symbolic_name_to_rid('trunk'); +** symbolic_name_to_rid('trunk','w'); +** +** Its result is either an RID or NULL. +*/ +void db_sym2rid_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char *arg; + const char *type; + if(1 != argc && 2 != argc){ + sqlite3_result_error(context, "Expecting one or two arguments", -1); + return; + } + arg = (const char*)sqlite3_value_text(argv[0]); + if(!arg){ + sqlite3_result_error(context, "Expecting a STRING argument", -1); + }else{ + int rid; + type = (2==argc) ? (const char*)sqlite3_value_text(argv[1]) : 0; + if(!type) type = "ci"; + rid = symbolic_name_to_rid( arg, type ); + if(rid<0){ + sqlite3_result_error(context, "Symbolic name is ambiguous.", -1); + }else if(0==rid){ + sqlite3_result_null(context); + }else{ + sqlite3_result_int64(context, rid); + } + } +} + + +/* +** SETTING: timeline-utc boolean default=on +** +** If the timeline-utc setting is true, then Fossil tries to understand +** and display all time values using UTC. If this setting is false, Fossil +** tries to understand and display time values using the local timezone. +** +** The word "timeline" in the name of this setting is historical. +** This setting applies to all user interfaces of Fossil, +** not just the timeline. +** +** Note that when accessing Fossil using the web interface, the localtime +** used is the localtime on the server, not on the client. +*/ +/* +** Return true if Fossil is set to display times as UTC. Return false +** if it wants to display times using the local timezone. +** +** False is returned if display is set to localtime even if the localtime +** happens to be the same as UTC. +*/ +int fossil_ui_utctime(void){ + if( g.fTimeFormat==0 ){ + if( db_get_int("timeline-utc", 1) ){ + g.fTimeFormat = 1; /* UTC */ + }else{ + g.fTimeFormat = 2; /* Localtime */ + } + } + return g.fTimeFormat==1; +} + +/* +** Return true if Fossil is set to display times using the local timezone. +*/ +int fossil_ui_localtime(void){ + return fossil_ui_utctime()==0; +} + +/* +** The toLocal() SQL function returns a string that is an argument to a +** date/time function that is appropriate for modifying the time for display. +** If UTC time display is selected, no modification occurs. If local time +** display is selected, the time is adjusted appropriately. +** +** Example usage: +** +** SELECT datetime('now',toLocal()); +*/ +void db_tolocal_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + if( fossil_ui_utctime() ){ + sqlite3_result_text(context, "0 seconds", -1, SQLITE_STATIC); + }else{ + sqlite3_result_text(context, "localtime", -1, SQLITE_STATIC); + } +} + +/* +** The fromLocal() SQL function returns a string that is an argument to a +** date/time function that is appropriate to convert an input time to UTC. +** If UTC time display is selected, no modification occurs. If local time +** display is selected, the time is adjusted from local to UTC. +** +** Example usage: +** +** SELECT julianday(:user_input,fromLocal()); +*/ +void db_fromlocal_function( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + if( fossil_ui_utctime() ){ + sqlite3_result_text(context, "0 seconds", -1, SQLITE_STATIC); + }else{ + sqlite3_result_text(context, "utc", -1, SQLITE_STATIC); + } +} + +/* +** If the input is a hexadecimal string, convert that string into a BLOB. +** If the input is not a hexadecimal string, return NULL. +*/ +void db_hextoblob( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const unsigned char *zIn = sqlite3_value_text(argv[0]); + int nIn = sqlite3_value_bytes(argv[0]); + unsigned char *zOut; + if( zIn==0 ) return; + if( nIn&1 ) return; + if( !validate16((const char*)zIn, nIn) ) return; + zOut = sqlite3_malloc64( nIn/2 + 1 ); + if( zOut==0 ){ + sqlite3_result_error_nomem(context); + return; + } + decode16(zIn, zOut, nIn); + sqlite3_result_blob(context, zOut, nIn/2, sqlite3_free); +} + +/* +** Return the XOR-obscured version of the input text. Useful for +** updating authentication strings in Fossil settings. To change +** the password locally stored for sync, for instance: +** +** echo "UPDATE config +** SET value = obscure('monkey123') +** WHERE name = 'last-sync-pw'" | +** fossil sql +** +** Note that user.pw uses a different obscuration algorithm, but +** you don't need to use 'fossil sql' for that anyway. Just call +** +** fossil user pass monkey123 +** +** to change the local user entry's password in the same way. +** +** 2022-12-30: If the user-data pointer is not NULL, then operate +** as unobscure() rather than obscure(). The obscure() variant of +** this routine is commonly available. But unobscure is (currently) +** only registered by the "fossil remote config-data --show-passwords" +** command. +*/ +void db_obscure( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const unsigned char *zIn = sqlite3_value_text(argv[0]); + int nIn = sqlite3_value_bytes(argv[0]); + char *zOut, *zTemp; + if( 0==zIn ) return; + if( 0==(zOut = sqlite3_malloc64( nIn * 2 + 3 )) ){ + sqlite3_result_error_nomem(context); + return; + } + if( sqlite3_user_data(context)==0 ){ + zTemp = obscure((char*)zIn); + }else{ + zTemp = unobscure((char*)zIn); + } + fossil_strcpy(zOut, zTemp); + fossil_free(zTemp); + sqlite3_result_text(context, zOut, strlen(zOut), sqlite3_free); +} + +/* +** Return True if zName is a protected (a.k.a. "sensitive") setting. +*/ +int db_setting_is_protected(const char *zName){ + const Setting *pSetting = zName ? db_find_setting(zName,0) : 0; + return pSetting!=0 && pSetting->sensitive!=0; +} + +/* +** Implement the protected_setting(X) SQL function. This function returns +** true if X is the name of a protected (security-sensitive) setting and +** the db.protectSensitive flag is enabled. It returns false otherwise. +*/ +LOCAL void db_protected_setting_func( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char *zSetting; + if( (db.protectMask & PROTECT_SENSITIVE)==0 ){ + sqlite3_result_int(context, 0); + return; + } + zSetting = (const char*)sqlite3_value_text(argv[0]); + sqlite3_result_int(context, db_setting_is_protected(zSetting)); +} + +/* +** Copied from SQLite ext/misc/uint.c... +** +** Compare text in lexicographic order, except strings of digits +** compare in numeric order. +** +** This version modified to also ignore case. +*/ +static int uintNocaseCollFunc( + void *notUsed, + int nKey1, const void *pKey1, + int nKey2, const void *pKey2 +){ + const unsigned char *zA = (const unsigned char*)pKey1; + const unsigned char *zB = (const unsigned char*)pKey2; + int i=0, j=0, x; + (void)notUsed; + while( i<nKey1 && j<nKey2 ){ + if( fossil_isdigit(zA[i]) && fossil_isdigit(zB[j]) ){ + int k; + while( i<nKey1 && zA[i]=='0' ){ i++; } + while( j<nKey2 && zB[j]=='0' ){ j++; } + k = 0; + while( i+k<nKey1 && fossil_isdigit(zA[i+k]) + && j+k<nKey2 && fossil_isdigit(zB[j+k]) ){ + k++; + } + if( i+k<nKey1 && fossil_isdigit(zA[i+k]) ){ + return +1; + }else if( j+k<nKey2 && fossil_isdigit(zB[j+k]) ){ + return -1; + }else{ + x = memcmp(zA+i, zB+j, k); + if( x ) return x; + i += k; + j += k; + } + }else + if( zA[i]!=zB[j] + && (x = fossil_tolower(zA[i]) - fossil_tolower(zB[j]))!=0 + ){ + return x; + }else{ + i++; + j++; + } + } + return (nKey1 - i) - (nKey2 - j); +} + + +/* +** Register the SQL functions that are useful both to the internal +** representation and to the "fossil sql" command. +*/ +void db_add_aux_functions(sqlite3 *db){ + sqlite3_create_collation(db, "uintnocase", SQLITE_UTF8,0,uintNocaseCollFunc); + sqlite3_create_function(db, "checkin_mtime", 2, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_checkin_mtime_function, 0, 0); + sqlite3_create_function(db, "symbolic_name_to_rid", 1, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_sym2rid_function, 0, 0); + sqlite3_create_function(db, "symbolic_name_to_rid", 2, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_sym2rid_function, 0, 0); + sqlite3_create_function(db, "now", 0, + SQLITE_UTF8|SQLITE_INNOCUOUS, 0, + db_now_function, 0, 0); + sqlite3_create_function(db, "toLocal", 0, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_tolocal_function, 0, 0); + sqlite3_create_function(db, "fromLocal", 0, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_fromlocal_function, 0, 0); + sqlite3_create_function(db, "hextoblob", 1, + SQLITE_UTF8|SQLITE_DETERMINISTIC|SQLITE_INNOCUOUS, + 0, db_hextoblob, 0, 0); + sqlite3_create_function(db, "capunion", 1, SQLITE_UTF8, 0, + 0, capability_union_step, capability_union_finalize); + sqlite3_create_function(db, "fullcap", 1, SQLITE_UTF8, 0, + capability_fullcap, 0, 0); + sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0, + alert_find_emailaddr_func, 0, 0); + sqlite3_create_function(db, "display_name", 1, SQLITE_UTF8, 0, + alert_display_name_func, 0, 0); + sqlite3_create_function(db, "obscure", 1, SQLITE_UTF8, 0, + db_obscure, 0, 0); + sqlite3_create_function(db, "protected_setting", 1, SQLITE_UTF8, 0, + db_protected_setting_func, 0, 0); + sqlite3_create_function(db, "win_reserved", 1, SQLITE_UTF8, 0, + db_win_reserved_func,0,0); + sqlite3_create_function(db, "url_nouser", 1, SQLITE_UTF8, 0, + url_nouser_func,0,0); + sqlite3_create_function(db, "chat_msg_from_event", 4, + SQLITE_UTF8 | SQLITE_INNOCUOUS, 0, + chat_msg_from_event, 0, 0); + sqlite3_create_function(db, "inode", 1, SQLITE_UTF8, 0, + file_inode_sql_func,0,0); + sqlite3_create_function(db, "artifact_to_json", 1, SQLITE_UTF8, 0, + artifact_to_json_sql_func,0,0); + +} + +#if USE_SEE +/* +** This is a pointer to the saved database encryption key string. +*/ +static char *zSavedKey = 0; + +/* +** This is the size of the saved database encryption key, in bytes. +*/ +static size_t savedKeySize = 0; + +/* +** This function returns non-zero if there is a saved database encryption +** key available. +*/ +int db_have_saved_encryption_key(){ + return db_is_valid_saved_encryption_key(zSavedKey, savedKeySize); +} + +/* +** This function returns non-zero if the specified database encryption key +** is valid. +*/ +int db_is_valid_saved_encryption_key(const char *p, size_t n){ + if( p==0 ) return 0; + if( n==0 ) return 0; + if( p[0]==0 ) return 0; + return 1; +} + +/* +** This function returns the saved database encryption key -OR- zero if +** no database encryption key is saved. +*/ +char *db_get_saved_encryption_key(){ + return zSavedKey; +} + +/* +** This function returns the size of the saved database encryption key +** -OR- zero if no database encryption key is saved. +*/ +size_t db_get_saved_encryption_key_size(){ + return savedKeySize; +} + +/* +** This function arranges for the saved database encryption key buffer +** to be allocated and then sets up the environment variable to allow +** a child process to initialize it with the actual database encryption +** key. +*/ +void db_setup_for_saved_encryption_key(){ + void *p = NULL; + size_t n = 0; + size_t pageSize = 0; + Blob pidKey; + + assert( !db_have_saved_encryption_key() ); + db_unsave_encryption_key(); + fossil_get_page_size(&pageSize); + assert( pageSize>0 ); + p = fossil_secure_alloc_page(&n); + assert( p!=NULL ); + assert( n==pageSize ); + blob_zero(&pidKey); + blob_appendf(&pidKey, "%lu:%p:%u", (unsigned long)GETPID(), p, n); + fossil_setenv("FOSSIL_SEE_PID_KEY", blob_str(&pidKey)); + zSavedKey = p; + savedKeySize = n; +} + +/* +** This function arranges for the database encryption key to be securely +** saved in non-pageable memory (on platforms where this is possible). +*/ +static void db_save_encryption_key( + Blob *pKey +){ + void *p = NULL; + size_t n = 0; + size_t pageSize = 0; + size_t blobSize = 0; + + assert( !db_have_saved_encryption_key() ); + blobSize = blob_size(pKey); + if( blobSize==0 ) return; + fossil_get_page_size(&pageSize); + assert( pageSize>0 ); + if( blobSize>pageSize ){ + fossil_panic("key blob too large: %u versus %u", blobSize, pageSize); + } + p = fossil_secure_alloc_page(&n); + assert( p!=NULL ); + assert( n==pageSize ); + assert( n>=blobSize ); + memcpy(p, blob_str(pKey), blobSize); + zSavedKey = p; + savedKeySize = n; +} + +/* +** This function arranges for the saved database encryption key to be +** securely zeroed, unlocked (if necessary), and freed. +*/ +void db_unsave_encryption_key(){ + fossil_secure_free_page(zSavedKey, savedKeySize); + zSavedKey = NULL; + savedKeySize = 0; +} + +/* +** This function sets the saved database encryption key to the specified +** string value, allocating or freeing the underlying memory if needed. +*/ +static void db_set_saved_encryption_key( + Blob *pKey +){ + if( zSavedKey!=NULL ){ + size_t blobSize = blob_size(pKey); + if( blobSize==0 ){ + db_unsave_encryption_key(); + }else{ + if( blobSize>savedKeySize ){ + fossil_panic("key blob too large: %u versus %u", + blobSize, savedKeySize); + } + fossil_secure_zero(zSavedKey, savedKeySize); + memcpy(zSavedKey, blob_str(pKey), blobSize); + } + }else{ + db_save_encryption_key(pKey); + } +} + +/* +** WEBPAGE: setseekey +** +** Sets the sets the saved database encryption key to one that gets passed +** via the "key" query string parameter. If the saved database encryption +** key has already been set, does nothing. This web page does not produce +** any output on success or failure. No permissions are required and none +** are checked (partially due to lack of encrypted database access). +** +** Query parameters: +** +** key The string to set as the saved database encryption +** key. +*/ +void db_set_see_key_page(void){ + Blob key; + const char *zKey; + if( db_have_saved_encryption_key() ){ + fossil_trace("SEE: encryption key was already set\n"); + return; + } + zKey = P("key"); + blob_init(&key, 0, 0); + if( zKey!=0 ){ + PID_T processId; + blob_set(&key, zKey); + db_set_saved_encryption_key(&key); + processId = db_maybe_handle_saved_encryption_key_for_process( + SEE_KEY_WRITE + ); + fossil_trace("SEE: set encryption key for process %lu, length %u\n", + (unsigned long)processId, blob_size(&key)); + }else{ + fossil_trace("SEE: no encryption key specified\n"); + } + blob_reset(&key); +} + +/* +** WEBPAGE: unsetseekey +** +** Sets the saved database encryption key to zeros in the current and parent +** Fossil processes. This web page does not produce any output on success +** or failure. Setup permission is required. +*/ +void db_unset_see_key_page(void){ + PID_T processId; + login_check_credentials(); + if( !g.perm.Setup ){ login_needed(0); return; } + processId = db_maybe_handle_saved_encryption_key_for_process( + SEE_KEY_ZERO + ); + fossil_trace("SEE: unset encryption key for process %lu\n", + (unsigned long)processId); +} + +/* +** This function reads the saved database encryption key from the +** specified Fossil parent process. This is only necessary (or +** functional) on Windows or Linux. +*/ +static void db_read_saved_encryption_key_from_process( + PID_T processId, /* Identifier for Fossil parent process. */ + LPVOID pAddress, /* Pointer to saved key buffer in the parent process. */ + SIZE_T nSize /* Size of saved key buffer in the parent process. */ +){ + void *p = NULL; + size_t n = 0; + size_t pageSize = 0; + + fossil_get_page_size(&pageSize); + assert( pageSize>0 ); + if( nSize>pageSize ){ + fossil_panic("key too large: %u versus %u", nSize, pageSize); + } + p = fossil_secure_alloc_page(&n); + assert( p!=NULL ); + assert( n==pageSize ); + assert( n>=nSize ); + { +#if defined(_WIN32) + HANDLE hProcess = OpenProcess(PROCESS_VM_READ, FALSE, processId); + if( hProcess!=NULL ){ + SIZE_T nRead = 0; + if( ReadProcessMemory(hProcess, pAddress, p, nSize, &nRead) ){ + CloseHandle(hProcess); + if( nRead==nSize ){ + db_unsave_encryption_key(); + zSavedKey = p; + savedKeySize = n; + }else{ + fossil_secure_free_page(p, n); + fossil_panic("bad size read, %u out of %u bytes at %p from pid %lu", + nRead, nSize, pAddress, processId); + } + }else{ + CloseHandle(hProcess); + fossil_secure_free_page(p, n); + fossil_panic("failed read, %u bytes at %p from pid %lu: %lu", nSize, + pAddress, processId, GetLastError()); + } + }else{ + fossil_secure_free_page(p, n); + fossil_panic("failed to open pid %lu: %lu", processId, GetLastError()); + } +#elif defined(__linux__) + ssize_t nRead; + struct iovec liov = {0}; + struct iovec riov = {0}; + liov.iov_base = p; + liov.iov_len = n; + riov.iov_base = pAddress; + riov.iov_len = nSize; + nRead = process_vm_readv(processId, &liov, 1, &riov, 1, 0); + if( nRead==nSize ){ + db_unsave_encryption_key(); + zSavedKey = p; + savedKeySize = n; + }else{ + fossil_secure_free_page(p, n); + fossil_panic("bad size read, %zd out of %zu bytes at %p from pid %lu", + nRead, nSize, pAddress, (unsigned long)processId); + } +#else + fossil_secure_free_page(p, n); + fossil_trace("db_read_saved_encryption_key_from_process unsupported"); +#endif + } +} + +/* +** This function writes the saved database encryption key into the +** specified Fossil parent process. This is only necessary (or +** functional) on Windows or Linux. +*/ +static void db_write_saved_encryption_key_to_process( + PID_T processId, /* Identifier for Fossil parent process. */ + LPVOID pAddress, /* Pointer to saved key buffer in the parent process. */ + SIZE_T nSize /* Size of saved key buffer in the parent process. */ +){ + void *p = db_get_saved_encryption_key(); + size_t n = db_get_saved_encryption_key_size(); + size_t pageSize = 0; + + fossil_get_page_size(&pageSize); + assert( pageSize>0 ); + if( nSize>pageSize ){ + fossil_panic("key too large: %u versus %u", nSize, pageSize); + } + assert( p!=NULL ); + assert( n==pageSize ); + assert( n>=nSize ); + { +#if defined(_WIN32) + HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_WRITE, + FALSE, processId); + if( hProcess!=NULL ){ + SIZE_T nWrite = 0; + if( WriteProcessMemory(hProcess, pAddress, p, nSize, &nWrite) ){ + CloseHandle(hProcess); + if( nWrite!=nSize ){ + fossil_panic("bad size write, %u out of %u bytes at %p from pid %lu", + nWrite, nSize, pAddress, processId); + } + }else{ + CloseHandle(hProcess); + fossil_panic("failed write, %u bytes at %p from pid %lu: %lu", nSize, + pAddress, processId, GetLastError()); + } + }else{ + fossil_panic("failed to open pid %lu: %lu", processId, GetLastError()); + } +#elif defined(__linux__) + ssize_t nWrite; + struct iovec liov = {0}; + struct iovec riov = {0}; + liov.iov_base = p; + liov.iov_len = n; + riov.iov_base = pAddress; + riov.iov_len = nSize; + nWrite = process_vm_writev(processId, &liov, 1, &riov, 1, 0); + if( nWrite!=nSize ){ + fossil_panic("bad size write, %zd out of %zu bytes at %p from pid %lu", + nWrite, nSize, pAddress, (unsigned long)processId); + } +#else + fossil_trace("db_write_saved_encryption_key_to_process unsupported"); +#endif + } +} + +/* +** This function zeros the saved database encryption key in the specified +** Fossil parent process. This is only necessary (or functional) on +** Windows or Linux. +*/ +static void db_zero_saved_encryption_key_in_process( + PID_T processId, /* Identifier for Fossil parent process. */ + LPVOID pAddress, /* Pointer to saved key buffer in the parent process. */ + SIZE_T nSize /* Size of saved key buffer in the parent process. */ +){ + void *p = NULL; + size_t n = 0; + size_t pageSize = 0; + + fossil_get_page_size(&pageSize); + assert( pageSize>0 ); + if( nSize>pageSize ){ + fossil_panic("key too large: %u versus %u", nSize, pageSize); + } + p = fossil_secure_alloc_page(&n); + assert( p!=NULL ); + assert( n==pageSize ); + assert( n>=nSize ); + { +#if defined(_WIN32) + HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION|PROCESS_VM_WRITE, + FALSE, processId); + if( hProcess!=NULL ){ + SIZE_T nWrite = 0; + if( WriteProcessMemory(hProcess, pAddress, p, nSize, &nWrite) ){ + CloseHandle(hProcess); + fossil_secure_free_page(p, n); + if( nWrite!=nSize ){ + fossil_panic("bad size zero, %u out of %u bytes at %p from pid %lu", + nWrite, nSize, pAddress, processId); + } + }else{ + CloseHandle(hProcess); + fossil_secure_free_page(p, n); + fossil_panic("failed zero, %u bytes at %p from pid %lu: %lu", nSize, + pAddress, processId, GetLastError()); + } + }else{ + fossil_secure_free_page(p, n); + fossil_panic("failed to open pid %lu: %lu", processId, GetLastError()); + } +#elif defined(__linux__) + ssize_t nWrite; + struct iovec liov = {0}; + struct iovec riov = {0}; + liov.iov_base = p; + liov.iov_len = n; + riov.iov_base = pAddress; + riov.iov_len = nSize; + nWrite = process_vm_writev(processId, &liov, 1, &riov, 1, 0); + if( nWrite!=nSize ){ + fossil_secure_free_page(p, n); + fossil_panic("bad size zero, %zd out of %zu bytes at %p from pid %lu", + nWrite, nSize, pAddress, (unsigned long)processId); + } +#else + fossil_secure_free_page(p, n); + fossil_trace("db_zero_saved_encryption_key_in_process unsupported"); +#endif + } +} + +/* +** This function evaluates the specified TH1 script and attempts to parse +** its result as a colon-delimited triplet containing a process identifier, +** address, and size (in bytes) of the database encryption key. This is +** only necessary (or functional) on Windows or Linux. +*/ +static PID_T db_handle_saved_encryption_key_for_process_via_th1( + const char *zConfig, /* The TH1 script to evaluate. */ + int eType /* Non-zero to write key to parent process -OR- + * zero to read it from the parent process. */ +){ + PID_T processId = 0; + int rc; + char *zResult; + char *zPwd = file_getcwd(0, 0); + Th_FossilInit(TH_INIT_DEFAULT | TH_INIT_NEED_CONFIG | TH_INIT_NO_REPO); + rc = Th_Eval(g.interp, 0, zConfig, -1); + zResult = (char*)Th_GetResult(g.interp, 0); + if( rc!=TH_OK ){ + fossil_fatal("script for pid key failed: %s", zResult); + } + if( zResult ){ + LPVOID pAddress = NULL; + SIZE_T nSize = 0; + parse_pid_key_value(zResult, &processId, &pAddress, &nSize); + if( eType==SEE_KEY_READ ){ + db_read_saved_encryption_key_from_process(processId, pAddress, nSize); + }else if( eType==SEE_KEY_WRITE ){ + db_write_saved_encryption_key_to_process(processId, pAddress, nSize); + }else if( eType==SEE_KEY_ZERO ){ + db_zero_saved_encryption_key_in_process(processId, pAddress, nSize); + }else{ + fossil_panic("unsupported SEE key operation %d", eType); + } + } + file_chdir(zPwd, 0); + fossil_free(zPwd); + return processId; +} + +/* +** This function sets the saved database encryption key to one that gets +** read from the specified Fossil parent process, if applicable. This is +** only necessary (or functional) on Windows or Linux. +*/ +PID_T db_maybe_handle_saved_encryption_key_for_process(int eType){ + PID_T processId = 0; + g.zPidKey = find_option("usepidkey",0,1); + if( !g.zPidKey ){ + g.zPidKey = fossil_getenv("FOSSIL_SEE_PID_KEY"); + } + if( g.zPidKey ){ + LPVOID pAddress = NULL; + SIZE_T nSize = 0; + parse_pid_key_value(g.zPidKey, &processId, &pAddress, &nSize); + if( eType==SEE_KEY_READ ){ + db_read_saved_encryption_key_from_process(processId, pAddress, nSize); + }else if( eType==SEE_KEY_WRITE ){ + db_write_saved_encryption_key_to_process(processId, pAddress, nSize); + }else if( eType==SEE_KEY_ZERO ){ + db_zero_saved_encryption_key_in_process(processId, pAddress, nSize); + }else{ + fossil_panic("unsupported SEE key operation %d", eType); + } + }else{ + const char *zSeeDbConfig = find_option("seedbcfg",0,1); + if( !zSeeDbConfig ){ + zSeeDbConfig = fossil_getenv("FOSSIL_SEE_DB_CONFIG"); + } + if( zSeeDbConfig ){ + processId = db_handle_saved_encryption_key_for_process_via_th1( + zSeeDbConfig, eType + ); + } + } + return processId; +} +#endif /* USE_SEE */ + +/* +** If the database file zDbFile has a name that suggests that it is +** encrypted, then prompt for the database encryption key and return it +** in the blob *pKey. Or, if the encryption key has previously been +** requested, just return a copy of the previous result. The blob in +** *pKey must be initialized. +*/ +static void db_maybe_obtain_encryption_key( + const char *zDbFile, /* Name of the database file */ + Blob *pKey /* Put the encryption key here */ +){ +#if USE_SEE + if( sqlite3_strglob("*.efossil", zDbFile)==0 ){ + char *zKey = db_get_saved_encryption_key(); + if( zKey ){ + blob_set(pKey, zKey); + }else{ + char *zPrompt = mprintf("\rencryption key for '%s': ", zDbFile); + prompt_for_password(zPrompt, pKey, 0); + fossil_free(zPrompt); + db_set_saved_encryption_key(pKey); + } + } +#endif +} + + +/* +** Sets the encryption key for the database, if necessary. +*/ +void db_maybe_set_encryption_key(sqlite3 *db, const char *zDbName){ + Blob key; + blob_init(&key, 0, 0); + db_maybe_obtain_encryption_key(zDbName, &key); + if( blob_size(&key)>0 ){ + if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){ + char *zCmd = sqlite3_mprintf("PRAGMA key(%Q)", blob_str(&key)); + sqlite3_exec(db, zCmd, 0, 0, 0); + fossil_secure_zero(zCmd, strlen(zCmd)); + sqlite3_free(zCmd); +#if USE_SEE + }else{ + sqlite3_key(db, blob_str(&key), -1); +#endif + } + } + blob_reset(&key); +} + +/* +** Open a database file. Return a pointer to the new database +** connection. An error results in process abort. +*/ +LOCAL sqlite3 *db_open(const char *zDbName){ + int rc; + sqlite3 *db; + Blob bNameCheck = BLOB_INITIALIZER; + + if( g.fSqlTrace ) fossil_trace("-- sqlite3_open: [%s]\n", zDbName); + file_canonical_name(zDbName, &bNameCheck, 0) + /* For purposes of the apndvfs check, g.nameOfExe and zDbName must + ** both be canonicalized, else chances are very good that they + ** will not match even if they're the same file. Details: + ** https://fossil-scm.org/forum/forumpost/16880a28aad1a868 */; + if( strcmp(blob_str(&bNameCheck), g.nameOfExe)==0 ){ + extern int sqlite3_appendvfs_init( + sqlite3 *, char **, const sqlite3_api_routines * + ); + sqlite3_appendvfs_init(0,0,0); + g.zVfsName = "apndvfs"; + } + blob_reset(&bNameCheck); + rc = sqlite3_open_v2( + zDbName, &db, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, + g.zVfsName + ); + if( rc!=SQLITE_OK ){ + db_err("[%s]: %s", zDbName, sqlite3_errmsg(db)); + } + db_maybe_set_encryption_key(db, zDbName); + sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_FKEY, 0, &rc); + sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_TRIGGER, 0, &rc); + sqlite3_db_config(db, SQLITE_DBCONFIG_TRUSTED_SCHEMA, 0, &rc); + sqlite3_db_config(db, SQLITE_DBCONFIG_DQS_DDL, 0, &rc); + sqlite3_db_config(db, SQLITE_DBCONFIG_DQS_DML, 0, &rc); + sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, &rc); + sqlite3_busy_timeout(db, 15000); + sqlite3_wal_autocheckpoint(db, 1); /* Set to checkpoint frequently */ + sqlite3_create_function(db, "user", 0, SQLITE_UTF8, 0, db_sql_user, 0, 0); + sqlite3_create_function(db, "cgi", 1, SQLITE_UTF8, 0, db_sql_cgi, 0, 0); + sqlite3_create_function(db, "cgi", 2, SQLITE_UTF8, 0, db_sql_cgi, 0, 0); + sqlite3_create_function(db, "print", -1, SQLITE_UTF8, 0,db_sql_print,0,0); + sqlite3_create_function( + db, "is_selected", 1, SQLITE_UTF8, 0, file_is_selected,0,0 + ); + sqlite3_create_function( + db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0 + ); + if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0); + db_add_aux_functions(db); + re_add_sql_func(db); /* The REGEXP operator */ + foci_register(db); /* The "files_of_checkin" virtual table */ + sqlite3_set_authorizer(db, db_top_authorizer, db); + db_register_fts5(db) /* in search.c */; + return db; +} + + +/* +** Detaches the zLabel database. +*/ +void db_detach(const char *zLabel){ + db_multi_exec("DETACH DATABASE %Q", zLabel); +} + +/* +** zDbName is the name of a database file. Attach zDbName using +** the name zLabel. +*/ +void db_attach(const char *zDbName, const char *zLabel){ + Blob key; + if( db_table_exists(zLabel,"sqlite_schema") ) return; + blob_init(&key, 0, 0); + db_maybe_obtain_encryption_key(zDbName, &key); + if( fossil_getenv("FOSSIL_USE_SEE_TEXTKEY")==0 ){ + char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY %Q", + zDbName, zLabel, blob_str(&key)); + db_exec_sql(zCmd); + fossil_secure_zero(zCmd, strlen(zCmd)); + sqlite3_free(zCmd); + }else{ + char *zCmd = sqlite3_mprintf("ATTACH DATABASE %Q AS %Q KEY ''", + zDbName, zLabel); + db_exec_sql(zCmd); + sqlite3_free(zCmd); +#if USE_SEE + if( blob_size(&key)>0 ){ + sqlite3_key_v2(g.db, zLabel, blob_str(&key), -1); + } +#endif + } + blob_reset(&key); +} + +/* +** Change the schema name of the "main" database to zLabel. +** zLabel must be a static string that is unchanged for the life of +** the database connection. +** +** After calling this routine, db_database_slot(zLabel) should +** return 0. +*/ +void db_set_main_schemaname(sqlite3 *db, const char *zLabel){ + if( sqlite3_db_config(db, SQLITE_DBCONFIG_MAINDBNAME, zLabel) ){ + fossil_panic("Fossil requires a version of SQLite that supports the " + "SQLITE_DBCONFIG_MAINDBNAME interface."); + } +} + +/* +** Return the slot number for database zLabel. The first database +** opened is slot 0. The "temp" database is slot 1. Attached databases +** are slots 2 and higher. +** +** Return -1 if zLabel does not match any open database. +*/ +int db_database_slot(const char *zLabel){ + int iSlot = -1; + int rc; + Stmt q; + if( g.db==0 ) return iSlot; + rc = db_prepare_ignore_error(&q, "PRAGMA database_list"); + if( rc==SQLITE_OK ){ + while( db_step(&q)==SQLITE_ROW ){ + if( fossil_strcmp(db_column_text(&q,1),zLabel)==0 ){ + iSlot = db_column_int(&q, 0); + break; + } + } + } + db_finalize(&q); + return iSlot; +} + +/* +** zDbName is the name of a database file. If no other database +** file is open, then open this one. If another database file is +** already open, then attach zDbName using the name zLabel. +*/ +void db_open_or_attach(const char *zDbName, const char *zLabel){ + if( !g.db ){ + g.db = db_open(zDbName); + db_set_main_schemaname(g.db, zLabel); + }else{ + db_attach(zDbName, zLabel); + } +} + +/* +** Close the per-user configuration database file +*/ +void db_close_config(){ + int iSlot = db_database_slot("configdb"); + if( iSlot>0 ){ + db_detach("configdb"); + }else if( g.dbConfig ){ + sqlite3_wal_checkpoint(g.dbConfig, 0); + sqlite3_close(g.dbConfig); + g.dbConfig = 0; + }else if( g.db && 0==iSlot ){ + int rc; + sqlite3_wal_checkpoint(g.db, 0); + rc = sqlite3_close(g.db); + if( g.fSqlTrace ) fossil_trace("-- db_close_config(%d)\n", rc); + g.db = 0; + g.repositoryOpen = 0; + g.localOpen = 0; + }else{ + return; + } + fossil_free(g.zConfigDbName); + g.zConfigDbName = 0; +} + +/* +** Compute the name of the configuration database. If unable to find the +** database, return 0 if isOptional is true, or panic if isOptional is false. +** +** Space to hold the result comes from fossil_malloc(). +*/ +static char *db_configdb_name(int isOptional){ + char *zHome; /* Home directory */ + char *zDbName; /* Name of the database file */ + + + /* On Windows, look for these directories, in order: + ** + ** FOSSIL_HOME + ** LOCALAPPDATA + ** APPDATA + ** USERPROFILE + ** HOMEDRIVE HOMEPATH + */ +#if defined(_WIN32) || defined(__CYGWIN__) + zHome = fossil_getenv("FOSSIL_HOME"); + if( zHome==0 ){ + zHome = fossil_getenv("LOCALAPPDATA"); + if( zHome==0 ){ + zHome = fossil_getenv("APPDATA"); + if( zHome==0 ){ + zHome = fossil_getenv("USERPROFILE"); + if( zHome==0 ){ + char *zDrive = fossil_getenv("HOMEDRIVE"); + char *zPath = fossil_getenv("HOMEPATH"); + if( zDrive && zPath ) zHome = mprintf("%s%s", zDrive, zPath); + } + } + } + } + zDbName = mprintf("%//_fossil", zHome); + fossil_free(zHome); + return zDbName; + +#else /* if unix */ + char *zXdgHome; + + /* For unix. a 5-step algorithm is used. + ** See ../www/tech_overview.wiki for discussion. + ** + ** Step 1: If FOSSIL_HOME exists -> $FOSSIL_HOME/.fossil + */ + zHome = fossil_getenv("FOSSIL_HOME"); + if( zHome!=0 ) return mprintf("%s/.fossil", zHome); + + /* Step 2: If HOME exists and file $HOME/.fossil exists -> $HOME/.fossil + */ + zHome = fossil_getenv("HOME"); + if( zHome ){ + zDbName = mprintf("%s/.fossil", zHome); + if( file_size(zDbName, ExtFILE)>1024*3 ){ + return zDbName; + } + fossil_free(zDbName); + } + + /* Step 3: if XDG_CONFIG_HOME exists -> $XDG_CONFIG_HOME/fossil.db + */ + zXdgHome = fossil_getenv("XDG_CONFIG_HOME"); + if( zXdgHome!=0 ){ + return mprintf("%s/fossil.db", zXdgHome); + } + + /* The HOME variable is required in order to continue. + */ + if( zHome==0 ){ + if( isOptional ) return 0; + fossil_fatal("cannot locate home directory - please set one of the " + "FOSSIL_HOME, XDG_CONFIG_HOME, or HOME environment " + "variables"); + } + + /* Step 4: If $HOME/.config is a directory -> $HOME/.config/fossil.db + */ + zXdgHome = mprintf("%s/.config", zHome); + if( file_isdir(zXdgHome, ExtFILE)==1 ){ + fossil_free(zXdgHome); + return mprintf("%s/.config/fossil.db", zHome); + } + + /* Step 5: Otherwise -> $HOME/.fossil + */ + return mprintf("%s/.fossil", zHome); +#endif /* unix */ +} + +/* +** Open the configuration database. Create the database anew if +** it does not already exist. +** +** If the useAttach flag is 0 (the usual case) then the configuration +** database is opened on a separate database connection g.dbConfig. +** This prevents the database from becoming locked on long check-in or sync +** operations which hold an exclusive transaction. In a few cases, though, +** it is convenient for the database to be attached to the main database +** connection so that we can join between the various databases. In that +** case, invoke this routine with useAttach as 1. +*/ +int db_open_config(int useAttach, int isOptional){ + char *zDbName; + if( g.zConfigDbName ){ + int alreadyAttached = db_database_slot("configdb")>0; + if( useAttach==alreadyAttached ) return 1; /* Already open. */ + db_close_config(); + } + zDbName = db_configdb_name(isOptional); + if( zDbName==0 ) return 0; + if( file_size(zDbName, ExtFILE)<1024*3 ){ + char *zHome = file_dirname(zDbName); + int rc; + if( file_isdir(zHome, ExtFILE)==0 ){ + file_mkdir(zHome, ExtFILE, 0); + } + rc = file_access(zHome, W_OK); + if( rc ){ + if( isOptional ) return 0; + fossil_fatal("home directory \"%s\" must be writeable", zHome); + } + db_init_database(zDbName, zConfigSchema, (char*)0); + fossil_free(zHome); + } + if( file_access(zDbName, W_OK) ){ + if( isOptional ) return 0; + fossil_fatal("configuration file %s must be writeable", zDbName); + } + if( useAttach ){ + db_open_or_attach(zDbName, "configdb"); + g.dbConfig = 0; + }else{ + g.dbConfig = db_open(zDbName); + db_set_main_schemaname(g.dbConfig, "configdb"); + } + g.zConfigDbName = zDbName; + return 1; +} + +/* +** Return TRUE if zTable exists. +*/ +int db_table_exists( + const char *zDb, /* One of: NULL, "configdb", "localdb", "repository" */ + const char *zTable /* Name of table */ +){ + return sqlite3_table_column_metadata(g.db, zDb, zTable, 0, + 0, 0, 0, 0, 0)==SQLITE_OK; +} + +/* +** Return TRUE if zTable exists and contains column zColumn. +** Return FALSE if zTable does not exist or if zTable exists +** but lacks zColumn. +*/ +int db_table_has_column( + const char *zDb, /* One of: NULL, "config", "localdb", "repository" */ + const char *zTable, /* Name of table */ + const char *zColumn /* Name of column in table */ +){ + return sqlite3_table_column_metadata(g.db, zDb, zTable, zColumn, + 0, 0, 0, 0, 0)==SQLITE_OK; +} + +/* +** Returns TRUE if zTable exists in the local database but lacks column +** zColumn +*/ +static int db_local_table_exists_but_lacks_column( + const char *zTable, + const char *zColumn +){ + return db_table_exists("localdb", zTable) + && !db_table_has_column("localdb", zTable, zColumn); +} + +/* +** If zDbName is a valid local database file, open it and return +** true. If it is not a valid local database file, return 0. +*/ +static int isValidLocalDb(const char *zDbName){ + i64 lsize; + + if( file_access(zDbName, F_OK) ) return 0; + lsize = file_size(zDbName, ExtFILE); + if( lsize%1024!=0 || lsize<4096 ) return 0; + db_open_or_attach(zDbName, "localdb"); + + /* Check to see if the check-out database has the latest schema changes. + ** The most recent schema change (2019-01-19) is the addition of the + ** vmerge.mhash and vfile.mhash fields. If the schema has the vmerge.mhash + ** column, assume everything else is up-to-date. + */ + if( db_table_has_column("localdb","vmerge","mhash") ){ + return 1; /* This is a check-out database with the latest schema */ + } + + /* If there is no vfile table, then assume we have picked up something + ** that is not even close to being a valid check-out database */ + if( !db_table_exists("localdb","vfile") ){ + return 0; /* Not a DB */ + } + + /* If the "isexe" column is missing from the vfile table, then + ** add it now. This code added on 2010-03-06. After all users have + ** upgraded, this code can be safely deleted. + */ + if( !db_table_has_column("localdb","vfile","isexe") ){ + db_multi_exec("ALTER TABLE vfile ADD COLUMN isexe BOOLEAN DEFAULT 0"); + } + + /* If "islink"/"isLink" columns are missing from tables, then + ** add them now. This code added on 2011-01-17 and 2011-08-27. + ** After all users have upgraded, this code can be safely deleted. + */ + if( !db_table_has_column("localdb","vfile","isLink") ){ + db_multi_exec("ALTER TABLE vfile ADD COLUMN islink BOOLEAN DEFAULT 0"); + if( db_local_table_exists_but_lacks_column("stashfile", "isLink") ){ + db_multi_exec("ALTER TABLE stashfile ADD COLUMN isLink BOOL DEFAULT 0"); + } + if( db_local_table_exists_but_lacks_column("undo", "isLink") ){ + db_multi_exec("ALTER TABLE undo ADD COLUMN isLink BOOLEAN DEFAULT 0"); + } + if( db_local_table_exists_but_lacks_column("undo_vfile", "islink") ){ + db_multi_exec("ALTER TABLE undo_vfile ADD COLUMN islink BOOL DEFAULT 0"); + } + } + + /* The design of the check-out database changed on 2019-01-19 adding the mhash + ** column to vfile and vmerge and changing the UNIQUE index on vmerge into + ** a PRIMARY KEY that includes the new mhash column. However, we must have + ** the repository database at hand in order to do the migration, so that + ** step is deferred. */ + return 1; +} + +/* +** Locate the root directory of the local repository tree. The root +** directory is found by searching for a file named "_FOSSIL_" or ".fslckout" +** that contains a valid repository database. +** +** For legacy, also look for ".fos". The use of ".fos" is deprecated +** since "fos" has negative connotations in Hungarian, we are told. +** +** If no valid _FOSSIL_ or .fslckout file is found, we move up one level and +** try again. Once the file is found, the g.zLocalRoot variable is set +** to the root of the repository tree and this routine returns 1. If +** no database is found, then this routine return 0. +** +** In db_open_local_v2(), if the bRootOnly flag is true, then only +** look in the CWD for the check-out database. Do not scan upwards in +** the file hierarchy. +** +** This routine always opens the user database regardless of whether or +** not the repository database is found. If the _FOSSIL_ or .fslckout file +** is found, it is attached to the open database connection too. +*/ +int db_open_local_v2(const char *zDbName, int bRootOnly){ + int i, n; + char zPwd[2000]; + static const char *(aDbName[]) = { "_FOSSIL_", ".fslckout", ".fos" }; + + if( g.localOpen ) return 1; + file_getcwd(zPwd, sizeof(zPwd)-20); + n = strlen(zPwd); + while( n>0 ){ + for(i=0; i<count(aDbName); i++){ + sqlite3_snprintf(sizeof(zPwd)-n, &zPwd[n], "/%s", aDbName[i]); + if( isValidLocalDb(zPwd) ){ + if( db_open_config(0, 1)==0 ){ + return 0; /* Configuration could not be opened */ + } + /* Found a valid check-out database file */ + g.zLocalDbName = fossil_strdup(zPwd); + zPwd[n] = 0; + while( n>0 && zPwd[n-1]=='/' ){ + n--; + zPwd[n] = 0; + } + g.zLocalRoot = mprintf("%s/", zPwd); + g.localOpen = 1; + db_open_repository(zDbName); + return 1; + } + } + if( bRootOnly ) break; + n--; + while( n>1 && zPwd[n]!='/' ){ n--; } + while( n>1 && zPwd[n-1]=='/' ){ n--; } + zPwd[n] = 0; + } + + /* A check-out database file could not be found */ + return 0; +} +int db_open_local(const char *zDbName){ + return db_open_local_v2(zDbName, 0); +} + +/* +** Get the full pathname to the repository database file. The +** local database (the _FOSSIL_ or .fslckout database) must have already +** been opened before this routine is called. +*/ +const char *db_repository_filename(void){ + static char *zRepo = 0; + assert( g.localOpen ); + assert( g.zLocalRoot ); + if( zRepo==0 ){ + zRepo = db_lget("repository", 0); + if( zRepo && !file_is_absolute_path(zRepo) ){ + char * zFree = zRepo; + zRepo = mprintf("%s%s", g.zLocalRoot, zRepo); + fossil_free(zFree); + zFree = zRepo; + zRepo = file_canonical_name_dup(zFree); + fossil_free(zFree); + } + } + return zRepo; +} + +/* +** Returns non-zero if support for symlinks is currently enabled. +*/ +int db_allow_symlinks(void){ + return g.allowSymlinks; +} + +/* +** Return TRUE if the file in the argument seems like it might be an +** SQLite database file that contains a Fossil repository schema. +*/ +int db_looks_like_a_repository(const char *zDbName){ + sqlite3 *db = 0; + i64 sz; + int rc; + int res = 0; + sqlite3_stmt *pStmt = 0; + + sz = file_size(zDbName, ExtFILE); + if( sz<16834 ) return 0; + db = db_open(zDbName); + if( !db ) return 0; + if( !g.zVfsName && sz%512 ) return 0; + rc = sqlite3_prepare_v2(db, + "SELECT count(*) FROM sqlite_schema" + " WHERE name COLLATE nocase IN" + "('blob','delta','rcvfrom','user','config','mlink','plink');", + -1, &pStmt, 0); + if( rc ) goto is_repo_end; + rc = sqlite3_step(pStmt); + if( rc!=SQLITE_ROW ) goto is_repo_end; + if( sqlite3_column_int(pStmt, 0)!=7 ) goto is_repo_end; + res = 1; + +is_repo_end: + sqlite3_finalize(pStmt); + sqlite3_close(db); + return res; +} + +/* +** COMMAND: test-is-repo +** Usage: %fossil test-is-repo FILENAME... +** +** Test whether the specified files look like a SQLite database +** containing a Fossil repository schema. +*/ +void test_is_repo(void){ + int i; + verify_all_options(); + for(i=2; i<g.argc; i++){ + fossil_print("%s: %s\n", + db_looks_like_a_repository(g.argv[i]) ? "yes" : " no", + g.argv[i] + ); + } +} + + +/* +** Open the repository database given by zDbName. If zDbName==NULL then +** get the name from the already open local database. +*/ +void db_open_repository(const char *zDbName){ + if( g.repositoryOpen ) return; + if( zDbName==0 ){ + if( g.localOpen ){ + zDbName = db_repository_filename(); + } + if( zDbName==0 ){ + db_err("unable to find the name of a repository database"); + } + } + if( !db_looks_like_a_repository(zDbName) ){ + if( file_access(zDbName, F_OK) ){ +#ifdef FOSSIL_ENABLE_JSON + g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; +#endif + fossil_fatal("repository does not exist or" + " is in an unreadable directory: %s", zDbName); + }else if( file_access(zDbName, R_OK) ){ +#ifdef FOSSIL_ENABLE_JSON + g.json.resultCode = FSL_JSON_E_DENIED; +#endif + fossil_fatal("read permission denied for repository %s", zDbName); + }else{ +#ifdef FOSSIL_ENABLE_JSON + g.json.resultCode = FSL_JSON_E_DB_NOT_VALID; +#endif + fossil_fatal("not a valid repository: %s", zDbName); + } + } + g.zRepositoryName = fossil_strdup(zDbName); + db_open_or_attach(g.zRepositoryName, "repository"); + g.repositoryOpen = 1; + sqlite3_file_control(g.db, "repository", SQLITE_FCNTL_DATA_VERSION, + &g.iRepoDataVers); + + /* Cache "allow-symlinks" option, because we'll need it on every stat call */ + g.allowSymlinks = db_get_boolean("allow-symlinks",0); + + g.zAuxSchema = db_get("aux-schema",""); + g.eHashPolicy = db_get_int("hash-policy",-1); + if( g.eHashPolicy<0 ){ + g.eHashPolicy = hname_default_policy(); + db_set_int("hash-policy", g.eHashPolicy, 0); + } + +#if 0 /* No longer automatic. Need to run "fossil rebuild" to migrate */ + /* Make a change to the CHECK constraint on the BLOB table for + ** version 2.0 and later. + */ + rebuild_schema_update_2_0(); /* Do the Fossil-2.0 schema updates */ +#endif + + /* Additional checks that occur when opening the check-out database */ + if( g.localOpen ){ + + /* If the repository database that was just opened has been + ** replaced by a clone of the same project, with different RID + ** values, then renumber the RID values stored in various tables + ** of the check-out database, so that the repository and check-out + ** databases align. + */ + if( !db_fingerprint_ok() ){ + if( find_option("no-rid-adjust",0,0)!=0 ){ + /* The --no-rid-adjust command-line option bypasses the RID value + ** updates. Intended for use during debugging, especially to be + ** able to run "fossil sql" after a database swap. */ + fossil_print( + "WARNING: repository change detected, but no adjust made.\n" + ); + }else if( find_option("rid-renumber-dryrun",0,0)!=0 ){ + /* the --rid-renumber-dryrun option shows how RID values would be + ** renumbered, but does not actually perform the renumbering. + ** This is a debugging-only option. */ + vfile_rid_renumbering_event(1); + exit(0); + }else{ + char *z; + stash_rid_renumbering_event(); + vfile_rid_renumbering_event(0); + undo_reset(); + bisect_reset(); + z = db_fingerprint(0, 1); + db_lset("fingerprint", z); + fossil_free(z); + fossil_print( + "WARNING: The repository database has been replaced by a clone.\n" + "Bisect history and undo have been lost.\n" + ); + } + } + + /* Make sure the check-out database schema migration of 2019-01-20 + ** has occurred. + ** + ** The 2019-01-19 migration is the addition of the vmerge.mhash and + ** vfile.mhash columns and making the vmerge.mhash column part of the + ** PRIMARY KEY for vmerge. + */ + if( !db_table_has_column("localdb", "vfile", "mhash") ){ + db_multi_exec("ALTER TABLE vfile ADD COLUMN mhash;"); + db_multi_exec( + "UPDATE vfile" + " SET mhash=(SELECT uuid FROM blob WHERE blob.rid=vfile.mrid)" + " WHERE mrid!=rid;" + ); + if( !db_table_has_column("localdb", "vmerge", "mhash") ){ + db_exec_sql("ALTER TABLE vmerge RENAME TO old_vmerge;"); + db_exec_sql(zLocalSchemaVmerge); + db_exec_sql( + "INSERT OR IGNORE INTO vmerge(id,merge,mhash)" + " SELECT id, merge, blob.uuid FROM old_vmerge, blob" + " WHERE old_vmerge.merge=blob.rid;" + "DROP TABLE old_vmerge;" + ); + } + } + } +} + +/* +** Return true if there have been any changes to the repository +** database since it was opened. +** +** Changes to "config" and "localdb" and "temp" do not count. +** This routine only returns true if there have been changes +** to "repository". +*/ +int db_repository_has_changed(void){ + unsigned int v; + if( !g.repositoryOpen ) return 0; + sqlite3_file_control(g.db, "repository", SQLITE_FCNTL_DATA_VERSION, &v); + return g.iRepoDataVers != v; +} + +/* +** Flags for the db_find_and_open_repository() function. +*/ +#if INTERFACE +#define OPEN_OK_NOT_FOUND 0x001 /* Do not error out if not found */ +#define OPEN_ANY_SCHEMA 0x002 /* Do not error if schema is wrong */ +#define OPEN_SUBSTITUTE 0x004 /* Fake in-memory repo if not found */ +#endif + +/* +** Try to find the repository and open it. Use the -R or --repository +** option to locate the repository. If no such option is available, then +** use the repository of the open check-out if there is one. +** +** Error out if the repository cannot be opened. +*/ +void db_find_and_open_repository(int bFlags, int nArgUsed){ + const char *zRep = find_repository_option(); + if( zRep && file_isdir(zRep, ExtFILE)==1 ){ + goto rep_not_found; + } + if( zRep==0 && nArgUsed && g.argc==nArgUsed+1 ){ + zRep = g.argv[nArgUsed]; + } + if( zRep==0 ){ + if( db_open_local(0)==0 ){ + goto rep_not_found; + } + zRep = db_repository_filename(); + if( zRep==0 ){ + goto rep_not_found; + } + } + db_open_repository(zRep); + if( g.repositoryOpen ){ + if( (bFlags & OPEN_ANY_SCHEMA)==0 ) db_verify_schema(); + return; + } +rep_not_found: + if( bFlags & OPEN_OK_NOT_FOUND ){ + /* No errors if the database is not found */ + if( bFlags & OPEN_SUBSTITUTE ){ + db_create_repository(0); + } + }else{ +#ifdef FOSSIL_ENABLE_JSON + g.json.resultCode = FSL_JSON_E_DB_NOT_FOUND; +#endif + if( nArgUsed==0 ){ + fossil_fatal("use --repository or -R to specify the repository database"); + }else{ + fossil_fatal("specify the repository name as a command-line argument"); + } + } +} + +/* +** Return TRUE if the schema is out-of-date +*/ +int db_schema_is_outofdate(void){ + return strcmp(g.zAuxSchema,AUX_SCHEMA_MIN)<0 + || strcmp(g.zAuxSchema,AUX_SCHEMA_MAX)>0; +} + +/* +** Return true if the database is writeable +*/ +int db_is_writeable(const char *zName){ + return g.db!=0 && !sqlite3_db_readonly(g.db, zName); +} + +/* +** Verify that the repository schema is correct. If it is not correct, +** issue a fatal error and die. +*/ +void db_verify_schema(void){ + if( db_schema_is_outofdate() ){ +#ifdef FOSSIL_ENABLE_JSON + g.json.resultCode = FSL_JSON_E_DB_NEEDS_REBUILD; +#endif + fossil_warning("incorrect repository schema version: " + "current repository schema version is \"%s\" " + "but need versions between \"%s\" and \"%s\".", + g.zAuxSchema, AUX_SCHEMA_MIN, AUX_SCHEMA_MAX); + fossil_fatal("run \"fossil rebuild\" to fix this problem"); + } +} + + +/* +** COMMAND: test-move-repository +** +** Usage: %fossil test-move-repository PATHNAME +** +** Change the location of the repository database on a local check-out. +** Use this command to avoid having to close and reopen a check-out +** when relocating the repository database. +*/ +void move_repo_cmd(void){ + Blob repo; + char *zRepo; + if( g.argc!=3 ){ + usage("PATHNAME"); + } + file_canonical_name(g.argv[2], &repo, 0); + zRepo = blob_str(&repo); + if( file_access(zRepo, F_OK) ){ + fossil_fatal("no such file: %s", zRepo); + } + if( db_open_local(zRepo)==0 ){ + fossil_fatal("not in a local check-out"); + return; + } + db_open_or_attach(zRepo, "test_repo"); + db_lset("repository", blob_str(&repo)); + db_record_repository_filename(blob_str(&repo)); + db_close(1); +} + + +/* +** Open the local database. If unable, exit with an error. +*/ +void db_must_be_within_tree(void){ + if( find_repository_option() ){ + fossil_fatal("the \"%s\" command only works from within an open check-out", + g.argv[1]); + } + if( db_open_local(0)==0 ){ + fossil_fatal("current directory is not within an open check-out"); + } + db_open_repository(0); + db_verify_schema(); +} + +/* +** Close the database connection. +** +** Check for unfinalized statements and report errors if the reportErrors +** argument is true. Ignore unfinalized statements when false. +*/ +void db_close(int reportErrors){ + sqlite3_stmt *pStmt; + if( g.db==0 ) return; + sqlite3_set_authorizer(g.db, 0, 0); + if( g.fSqlStats ){ + int cur, hiwtr; + sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_USED, &cur, &hiwtr, 0); + fprintf(stderr, "-- LOOKASIDE_USED %10d %10d\n", cur, hiwtr); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_HIT, &cur, &hiwtr, 0); + fprintf(stderr, "-- LOOKASIDE_HIT %10d\n", hiwtr); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_MISS_SIZE, &cur,&hiwtr,0); + fprintf(stderr, "-- LOOKASIDE_MISS_SIZE %10d\n", hiwtr); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_LOOKASIDE_MISS_FULL, &cur,&hiwtr,0); + fprintf(stderr, "-- LOOKASIDE_MISS_FULL %10d\n", hiwtr); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_CACHE_USED, &cur, &hiwtr, 0); + fprintf(stderr, "-- CACHE_USED %10d\n", cur); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_SCHEMA_USED, &cur, &hiwtr, 0); + fprintf(stderr, "-- SCHEMA_USED %10d\n", cur); + sqlite3_db_status(g.db, SQLITE_DBSTATUS_STMT_USED, &cur, &hiwtr, 0); + fprintf(stderr, "-- STMT_USED %10d\n", cur); + sqlite3_status(SQLITE_STATUS_MEMORY_USED, &cur, &hiwtr, 0); + fprintf(stderr, "-- MEMORY_USED %10d %10d\n", cur, hiwtr); + sqlite3_status(SQLITE_STATUS_MALLOC_SIZE, &cur, &hiwtr, 0); + fprintf(stderr, "-- MALLOC_SIZE %10d\n", hiwtr); + sqlite3_status(SQLITE_STATUS_MALLOC_COUNT, &cur, &hiwtr, 0); + fprintf(stderr, "-- MALLOC_COUNT %10d %10d\n", cur, hiwtr); + sqlite3_status(SQLITE_STATUS_PAGECACHE_OVERFLOW, &cur, &hiwtr, 0); + fprintf(stderr, "-- PCACHE_OVFLOW %10d %10d\n", cur, hiwtr); + fprintf(stderr, "-- prepared statements %10d\n", db.nPrepare); + } + while( db.pAllStmt ){ + db_finalize(db.pAllStmt); + } + if( db.nBegin ){ + if( reportErrors ){ + fossil_warning("Transaction started at %s:%d never commits", + db.zStartFile, db.iStartLine); + } + db_end_transaction(1); + } + pStmt = 0; + sqlite3_busy_timeout(g.db, 0); + g.dbIgnoreErrors++; /* Stop "database locked" warnings */ + sqlite3_exec(g.db, "PRAGMA optimize", 0, 0, 0); + g.dbIgnoreErrors--; + db_close_config(); + + /* If the localdb has a lot of unused free space, + ** then VACUUM it as we shut down. + */ + if( db_database_slot("localdb")>=0 ){ + int nFree = db_int(0, "PRAGMA localdb.freelist_count"); + int nTotal = db_int(0, "PRAGMA localdb.page_count"); + if( nFree>nTotal/4 ){ + db_unprotect(PROTECT_ALL); + db_multi_exec("VACUUM localdb;"); + db_protect_pop(); + } + } + + if( g.db ){ + int rc; + sqlite3_wal_checkpoint(g.db, 0); + rc = sqlite3_close(g.db); + if( g.fSqlTrace ) fossil_trace("-- sqlite3_close(%d)\n", rc); + if( rc==SQLITE_BUSY && reportErrors ){ + while( (pStmt = sqlite3_next_stmt(g.db, pStmt))!=0 ){ + fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt)); + } + } + g.db = 0; + } + g.repositoryOpen = 0; + g.localOpen = 0; + db.bProtectTriggers = 0; + assert( g.dbConfig==0 ); + assert( g.zConfigDbName==0 ); + backoffice_run_if_needed(); +} + +/* +** Close the database as quickly as possible without unnecessary processing. +*/ +void db_panic_close(void){ + if( g.db ){ + int rc; + sqlite3_wal_checkpoint(g.db, 0); + rc = sqlite3_close(g.db); + if( g.fSqlTrace ) fossil_trace("-- sqlite3_close(%d)\n", rc); + db_clear_authorizer(); + } + g.db = 0; + g.repositoryOpen = 0; + g.localOpen = 0; +} + +/* +** Create a new empty repository database with the given name. +** +** Only the schema is initialized. The required VAR tables entries +** are not set by this routine and must be set separately in order +** to make the new file a valid database. +*/ +void db_create_repository(const char *zFilename){ + db_init_database( + zFilename, + zRepositorySchema1, + zRepositorySchemaDefaultReports, + zRepositorySchema2, + (char*)0 + ); + db_delete_on_failure(zFilename); +} + +/* +** Create the default user accounts in the USER table. +*/ +void db_create_default_users(int setupUserOnly, const char *zDefaultUser){ + const char *zUser = zDefaultUser; + if( zUser==0 ){ + zUser = db_get("default-user", 0); + } + if( zUser==0 ){ + zUser = fossil_getenv("FOSSIL_USER"); + } + if( zUser==0 ){ + zUser = fossil_getenv("USER"); + } + if( zUser==0 ){ + zUser = fossil_getenv("LOGNAME"); + } + if( zUser==0 ){ + zUser = fossil_getenv("USERNAME"); + } + if( zUser==0 ){ + zUser = "root"; + } + db_unprotect(PROTECT_USER); + db_multi_exec( + "INSERT OR IGNORE INTO user(login, info) VALUES(%Q,'')", zUser + ); + db_multi_exec( + "UPDATE user SET cap='s', pw=%Q" + " WHERE login=%Q", fossil_random_password(10), zUser + ); + if( !setupUserOnly ){ + db_multi_exec( + "INSERT OR IGNORE INTO user(login,pw,cap,info)" + " VALUES('anonymous',hex(randomblob(8)),'hz','Anon');" + "INSERT OR IGNORE INTO user(login,pw,cap,info)" + " VALUES('nobody','','gjorz','Nobody');" + "INSERT OR IGNORE INTO user(login,pw,cap,info)" + " VALUES('developer','','ei','Dev');" + "INSERT OR IGNORE INTO user(login,pw,cap,info)" + " VALUES('reader','','kptw','Reader');" + ); + } + db_protect_pop(); +} + +/* +** Return a pointer to a string that contains the RHS of an IN operator +** that will select CONFIG table names that are in the list of control +** settings. +*/ +const char *db_setting_inop_rhs(){ + Blob x; + int i; + int nSetting; + const Setting *aSetting = setting_info(&nSetting); + const char *zSep = ""; + + blob_zero(&x); + blob_append_sql(&x, "("); + for(i=0; i<nSetting; i++){ + blob_append_sql(&x, "%s%Q", zSep/*safe-for-%s*/, aSetting[i].name); + zSep = ","; + } + blob_append_sql(&x, ")"); + return blob_sql_text(&x); +} + +/* +** Fill an empty repository database with the basic information for a +** repository. This function is shared between 'create_repository_cmd' +** ('new') and 'reconstruct_cmd' ('reconstruct'), both of which create +** new repositories. +** +** The zTemplate parameter determines if the settings for the repository +** should be copied from another repository. If zTemplate is 0 then the +** settings will have their normal default values. If zTemplate is +** non-zero, it is assumed that the caller of this function has already +** attached a database using the label "settingSrc". If not, the call to +** this function will fail. +** +** The zInitialDate parameter determines the date of the initial check-in +** that is automatically created. If zInitialDate is 0 then no initial +** check-in is created. The makeServerCodes flag determines whether or +** not server and project codes are invented for this repository. +*/ +void db_initial_setup( + const char *zTemplate, /* Repository from which to copy settings. */ + const char *zInitialDate, /* Initial date of repository. (ex: "now") */ + const char *zDefaultUser /* Default user for the repository */ +){ + char *zDate; + Blob hash; + Blob manifest; + + db_unprotect(PROTECT_ALL); + db_set("content-schema", CONTENT_SCHEMA, 0); + db_set("aux-schema", AUX_SCHEMA_MAX, 0); + db_set("rebuilt", get_version(), 0); + db_multi_exec( + "INSERT INTO config(name,value,mtime)" + " VALUES('server-code', lower(hex(randomblob(20))),now());" + "INSERT INTO config(name,value,mtime)" + " VALUES('project-code', lower(hex(randomblob(20))),now());" + ); + db_create_default_users(0, zDefaultUser); + if( zDefaultUser ) g.zLogin = zDefaultUser; + user_select(); + + if( zTemplate ){ + /* + ** Copy all settings from the supplied template repository. + */ + db_multi_exec( + "INSERT OR REPLACE INTO config" + " SELECT name,value,mtime FROM settingSrc.config" + " WHERE (name IN %s OR name IN %s OR name GLOB 'walias:/*')" + " AND name NOT GLOB 'project-*'" + " AND name NOT GLOB 'short-project-*';", + configure_inop_rhs(CONFIGSET_ALL), + db_setting_inop_rhs() + ); + g.eHashPolicy = db_get_int("hash-policy", g.eHashPolicy); + db_multi_exec( + "REPLACE INTO reportfmt SELECT * FROM settingSrc.reportfmt;" + ); + + /* + ** Copy the user permissions, contact information, last modified + ** time, and photo for all the "system" users from the supplied + ** template repository into the one being setup. The other columns + ** are not copied because they contain security information or other + ** data specific to the other repository. The list of columns copied + ** by this SQL statement may need to be revised in the future. + */ + db_multi_exec("UPDATE user SET" + " cap = (SELECT u2.cap FROM settingSrc.user u2" + " WHERE u2.login = user.login)," + " info = (SELECT u2.info FROM settingSrc.user u2" + " WHERE u2.login = user.login)," + " mtime = (SELECT u2.mtime FROM settingSrc.user u2" + " WHERE u2.login = user.login)," + " photo = (SELECT u2.photo FROM settingSrc.user u2" + " WHERE u2.login = user.login)" + " WHERE user.login IN ('anonymous','nobody','developer','reader');" + ); + } + db_protect_pop(); + + if( zInitialDate ){ + int rid; + blob_zero(&manifest); + blob_appendf(&manifest, "C initial\\sempty\\scheck-in\n"); + zDate = date_in_standard_format(zInitialDate); + blob_appendf(&manifest, "D %s\n", zDate); + md5sum_init(); + /* The R-card is necessary here because without it + * fossil versions earlier than versions 1.27 would + * interpret this artifact as a "control". */ + blob_appendf(&manifest, "R %s\n", md5sum_finish(0)); + blob_appendf(&manifest, "T *branch * trunk\n"); + blob_appendf(&manifest, "T *sym-trunk *\n"); + blob_appendf(&manifest, "U %F\n", g.zLogin); + md5sum_blob(&manifest, &hash); + blob_appendf(&manifest, "Z %b\n", &hash); + blob_reset(&hash); + rid = content_put(&manifest); + manifest_crosslink(rid, &manifest, MC_NONE); + } +} + +/* +** COMMAND: new# +** COMMAND: init +** +** Usage: %fossil new ?OPTIONS? FILENAME +** or: %fossil init ?OPTIONS? FILENAME +** +** Create a repository for a new project in the file named FILENAME. +** This command is distinct from "clone". The "clone" command makes +** a copy of an existing project. This command starts a new project. +** +** By default, your current login name is used to create the default +** admin user. This can be overridden using the -A|--admin-user +** parameter. +** +** By default, all settings will be initialized to their default values. +** This can be overridden using the --template parameter to specify a +** repository file from which to copy the initial settings. When a template +** repository is used, almost all of the settings accessible from the setup +** page, either directly or indirectly, will be copied. Normal users and +** their associated permissions will not be copied; however, the system +** default users "anonymous", "nobody", "reader", "developer", and their +** associated permissions will be copied. In case of SQL errors, rebuild the +** template repository and try again. +** +** Options: +** --template FILE Copy settings from repository file +** -A|--admin-user USERNAME Select given USERNAME as admin user +** --date-override DATETIME Use DATETIME as time of the initial check-in +** --sha1 Use an initial hash policy of "sha1" +** --project-name STRING The name of the project "project name in +** quotes" +** --project-desc STRING The description of the project "project +** description in quotes" +** +** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in +** year-month-day form, it may be truncated, the "T" may be replaced by +** a space, and it may also name a timezone offset from UTC as "-HH:MM" +** (westward) or "+HH:MM" (eastward). Either no timezone suffix or "Z" +** means UTC. +** +** See also: [[clone]] +*/ +void create_repository_cmd(void){ + char *zPassword; + const char *zTemplate; /* Repository from which to copy settings */ + const char *zDate; /* Date of the initial check-in */ + const char *zDefaultUser; /* Optional name of the default user */ + const char *zProjectName; /* Optional project name of the repo */ + const char *zProjectDesc; /* Optional project description "description + ** of project in quotes" */ + int bUseSha1 = 0; /* True to set the hash-policy to sha1 */ + + + zTemplate = find_option("template",0,1); + zDate = find_option("date-override",0,1); + zDefaultUser = find_option("admin-user","A",1); + bUseSha1 = find_option("sha1",0,0)!=0; + zProjectName = find_option("project-name", 0, 1); + zProjectDesc = find_option("project-desc", 0, 1); + /* We should be done with options.. */ + verify_all_options(); + + if( g.argc!=3 ){ + usage("REPOSITORY-NAME"); + } + + if( -1 != file_size(g.argv[2], ExtFILE) ){ + fossil_fatal("file already exists: %s", g.argv[2]); + } + + db_create_repository(g.argv[2]); + db_open_repository(g.argv[2]); + db_open_config(0, 0); + if( zTemplate ) db_attach(zTemplate, "settingSrc"); + db_begin_transaction(); + if( bUseSha1 ){ + g.eHashPolicy = HPOLICY_SHA1; + db_set_int("hash-policy", HPOLICY_SHA1, 0); + } + if( zProjectName ) db_set("project-name", zProjectName, 0); + if( zProjectDesc ) db_set("project-description", zProjectDesc, 0); + if( zDate==0 ) zDate = "now"; + db_initial_setup(zTemplate, zDate, zDefaultUser); + db_end_transaction(0); + if( zTemplate ) db_detach("settingSrc"); + if( zProjectName ) fossil_print("project-name: %s\n", zProjectName); + if( zProjectDesc ) fossil_print("project-description: %s\n", zProjectDesc); + fossil_print("project-id: %s\n", db_get("project-code", 0)); + fossil_print("server-id: %s\n", db_get("server-code", 0)); + zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin); + fossil_print("admin-user: %s (initial remote-access password is \"%s\")\n", + g.zLogin, zPassword); + hash_user_password(g.zLogin); +} + +/* +** SQL functions for debugging. +** +** The print() function writes its arguments on stdout, but only +** if the -sqlprint command-line option is turned on. +*/ +void db_sql_print( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + int i; + if( g.fSqlPrint ){ + for(i=0; i<argc; i++){ + char c = i==argc-1 ? '\n' : ' '; + fossil_print("%s%c", sqlite3_value_text(argv[i]), c); + } + } +} + +/* +** Callback for sqlite3_trace_v2(); +*/ +int db_sql_trace(unsigned m, void *notUsed, void *pP, void *pX){ + sqlite3_stmt *pStmt = (sqlite3_stmt*)pP; + char *zSql; + int n; + const char *zArg = (const char*)pX; + char zEnd[100]; + if( m & SQLITE_TRACE_CLOSE ){ + /* If we are tracking closes, that means we want to clean up static + ** prepared statements. */ + while( db.pAllStmt ){ + db_finalize(db.pAllStmt); + } + return 0; + } + if( zArg[0]=='-' ) return 0; + if( m & SQLITE_TRACE_PROFILE ){ + sqlite3_int64 nNano = *(sqlite3_int64*)pX; + double rMillisec = 0.000001 * nNano; + int nRun = sqlite3_stmt_status(pStmt, SQLITE_STMTSTATUS_RUN, 0); + int nVmStep = sqlite3_stmt_status(pStmt, SQLITE_STMTSTATUS_VM_STEP, 1); + sqlite3_snprintf(sizeof(zEnd),zEnd," /* %.3fms, %r run, %d vm-steps */\n", + rMillisec, nRun, nVmStep); + }else{ + zEnd[0] = '\n'; + zEnd[1] = 0; + } + zSql = sqlite3_expanded_sql(pStmt); + n = (int)strlen(zSql); + fossil_trace("%s%s%s", zSql, (n>0 && zSql[n-1]==';') ? "" : ";", zEnd); + sqlite3_free(zSql); + return 0; +} + +/* +** Implement the user() SQL function. user() takes no arguments and +** returns the user ID of the current user. +*/ +LOCAL void db_sql_user( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + if( g.zLogin!=0 ){ + sqlite3_result_text(context, g.zLogin, -1, SQLITE_STATIC); + } +} + +/* +** Implement the cgi() SQL function. cgi() takes an argument which is +** a name of CGI query parameter. The value of that parameter is returned, +** if available. Optional second argument will be returned if the first +** doesn't exist as a CGI parameter. +*/ +LOCAL void db_sql_cgi(sqlite3_context *context, int argc, sqlite3_value **argv){ + const char* zP; + if( argc!=1 && argc!=2 ) return; + zP = P((const char*)sqlite3_value_text(argv[0])); + if( zP ){ + sqlite3_result_text(context, zP, -1, SQLITE_STATIC); + }else if( argc==2 ){ + zP = (const char*)sqlite3_value_text(argv[1]); + if( zP ) sqlite3_result_text(context, zP, -1, SQLITE_TRANSIENT); + } +} + +/* +** SQL function: +** +** is_selected(id) +** if_selected(id, X, Y) +** +** On the commit command, when filenames are specified (in order to do +** a partial commit) the vfile.id values for the named files are loaded +** into the g.aCommitFile[] array. This function looks at that array +** to see if a file is named on the command-line. +** +** In the first form (1 argument) return TRUE if either no files are +** named on the command line (g.aCommitFile is NULL meaning that all +** changes are to be committed) or if id is found in g.aCommitFile[] +** (meaning that id was named on the command-line). +** +** In the second form (3 arguments) return argument X if true and Y +** if false. Except if Y is NULL then always return X. +*/ +LOCAL void file_is_selected( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + int rc = 0; + + assert(argc==1 || argc==3); + if( g.aCommitFile ){ + int iId = sqlite3_value_int(argv[0]); + int ii; + for(ii=0; g.aCommitFile[ii]; ii++){ + if( iId==g.aCommitFile[ii] ){ + rc = 1; + break; + } + } + }else{ + rc = 1; + } + if( argc==1 ){ + sqlite3_result_int(context, rc); + }else{ + assert( argc==3 ); + assert( rc==0 || rc==1 ); + if( sqlite3_value_type(argv[2-rc])==SQLITE_NULL ) rc = 1-rc; + sqlite3_result_value(context, argv[2-rc]); + } +} + +/* +** Implementation of the "win_reserved(X)" SQL function, a wrapper +** for file_is_win_reserved(X) which returns true if X is +** a Windows-reserved filename. +*/ +LOCAL void db_win_reserved_func( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char * zName = (const char *)sqlite3_value_text(argv[0]); + if( zName!=0 ){ + sqlite3_result_int(context, file_is_win_reserved(zName)!=0); + } +} + +/* +** Convert the input string into an artifact hash. Make a notation in the +** CONCEALED table so that the hash can be undo using the db_reveal() +** function at some later time. +** +** The value returned is stored in static space and will be overwritten +** on subsequent calls. +** +** If zContent is already a well-formed artifact hash, then return a copy +** of that hash, not a hash of the hash. +** +** The CONCEALED table is meant to obscure email addresses. Every valid +** email address will contain a "@" character and "@" is not valid within +** a SHA1 hash so there is no chance that a valid email address will go +** unconcealed. +*/ +char *db_conceal(const char *zContent, int n){ + static char zHash[HNAME_MAX+1]; + Blob out; + if( hname_validate(zContent, n) ){ + memcpy(zHash, zContent, n); + zHash[n] = 0; + }else{ + sha1sum_step_text(zContent, n); + sha1sum_finish(&out); + sqlite3_snprintf(sizeof(zHash), zHash, "%s", blob_str(&out)); + blob_reset(&out); + db_multi_exec( + "INSERT OR IGNORE INTO concealed(hash,content,mtime)" + " VALUES(%Q,%#Q,now())", + zHash, n, zContent + ); + } + return zHash; +} + +/* +** Attempt to look up the input in the CONCEALED table. If found, +** and if the okRdAddr permission is enabled then return the +** original value for which the input is a hash. If g.perm.RdAddr is +** false or if the lookup fails, return the original string content. +** +** In either case, the string returned is stored in space obtained +** from malloc and should be freed by the calling function. +*/ +char *db_reveal(const char *zKey){ + char *zOut; + if( g.perm.RdAddr ){ + zOut = db_text(0, "SELECT content FROM concealed WHERE hash=%Q", zKey); + }else{ + zOut = 0; + } + if( zOut==0 ){ + zOut = fossil_strdup_nn(zKey); + } + return zOut; +} + +/* +** Return true if the string zVal represents "true" (or "false"). +*/ +int is_truth(const char *zVal){ + static const char *const azOn[] = { "on", "yes", "true" }; + int i; + for(i=0; i<count(azOn); i++){ + if( fossil_stricmp(zVal,azOn[i])==0 ) return 1; + } + return atoi(zVal); +} +int is_false(const char *zVal){ + static const char *const azOff[] = { "off", "no", "false", "0" }; + int i; + for(i=0; i<count(azOff); i++){ + if( fossil_stricmp(zVal,azOff[i])==0 ) return 1; + } + return 0; +} + +/* +** Swap the g.db and g.dbConfig connections so that the various db_* routines +** work on the configuration database instead of on the repository database. +** Be sure to swap them back after doing the operation. +** +** If the configuration database has already been opened as the main database +** or is attached to the main database, no connection swaps are required so +** this routine is a no-op. +*/ +void db_swap_connections(void){ + /* + ** When swapping the main database connection with the config database + ** connection, the config database connection must be open (not simply + ** attached); otherwise, the swap would end up leaving the main database + ** connection invalid, defeating the very purpose of this routine. This + ** same constraint also holds true when restoring the previously swapped + ** database connection; otherwise, it means that no swap was performed + ** because the main database connection was already pointing to the config + ** database. + */ + if( g.dbConfig ){ + sqlite3 *dbTemp = g.db; + g.db = g.dbConfig; + g.dbConfig = dbTemp; + } +} + +/* +** Try to read a versioned setting string from .fossil-settings/<name>. +** +** Return the text of the string if it is found. Return NULL if not +** found. +** +** If the zNonVersionedSetting parameter is not NULL then it holds the +** non-versioned value for this setting. If both a versioned and a +** non-versioned value exist and are not equal, then a warning message +** might be generated. +** +** zCkin is normally NULL. In that case, the versioned setting is +** take from the local check-out, if a local checkout exists, or from +** check-in named by the g.zOpenRevision global variable. If zCkin is +** not NULL, then zCkin is the name of the specific check-in from which +** versioned setting value is taken. When zCkin is not NULL, the cache +** is bypassed. +*/ +char *db_get_versioned( + const char *zName, + char *zNonVersionedSetting, + const char *zCkin +){ + char *zVersionedSetting = 0; + int noWarn = 0; + int found = 0; + struct _cacheEntry { + struct _cacheEntry *next; + const char *zName, *zValue; + } *cacheEntry = 0; + static struct _cacheEntry *cache = 0; + + if( !g.localOpen && g.zOpenRevision==0 && zCkin==0 ){ + return zNonVersionedSetting; + } + + /* Look up name in cache */ + if( zCkin==0 ){ + cacheEntry = cache; + while( cacheEntry!=0 ){ + if( fossil_strcmp(cacheEntry->zName, zName)==0 ){ + zVersionedSetting = fossil_strdup(cacheEntry->zValue); + break; + } + cacheEntry = cacheEntry->next; + } + } + + /* Attempt to read value from file in check-out if there wasn't a cache hit.*/ + if( cacheEntry==0 ){ + Blob versionedPathname; + Blob setting; + blob_init(&versionedPathname, 0, 0); + blob_init(&setting, 0, 0); + if( !g.localOpen || zCkin!=0 ){ + /* Repository is in the process of being opened, but files have not been + * written to disk. Load from the database. */ + blob_appendf(&versionedPathname, ".fossil-settings/%s", zName); + if( historical_blob(zCkin ? zCkin : g.zOpenRevision, + blob_str(&versionedPathname), + &setting, 0) + ){ + found = 1; + } + }else{ + blob_appendf(&versionedPathname, "%s.fossil-settings/%s", + g.zLocalRoot, zName); + if( file_size(blob_str(&versionedPathname), ExtFILE)>=0 ){ + /* File exists, and contains the value for this setting. Load from + ** the file. */ + const char *zFile = blob_str(&versionedPathname); + if( blob_read_from_file(&setting, zFile, ExtFILE)>=0 ){ + found = 1; + } + /* See if there's a no-warn flag */ + blob_append(&versionedPathname, ".no-warn", -1); + if( file_size(blob_str(&versionedPathname), ExtFILE)>=0 ){ + noWarn = 1; + } + } + } + blob_reset(&versionedPathname); + if( found ){ + blob_strip_comment_lines(&setting, &setting); + blob_trim(&setting); /* Avoid non-obvious problems with line endings + ** on boolean properties */ + zVersionedSetting = fossil_strdup(blob_str(&setting)); + } + blob_reset(&setting); + + /* Store result in cache, which can be the value or 0 if not found */ + if( zCkin==0 ){ + cacheEntry = (struct _cacheEntry*)fossil_malloc(sizeof(*cacheEntry)); + cacheEntry->next = cache; + cacheEntry->zName = zName; + cacheEntry->zValue = fossil_strdup(zVersionedSetting); + cache = cacheEntry; + } + } + + /* Display a warning? */ + if( zVersionedSetting!=0 + && zNonVersionedSetting!=0 + && zNonVersionedSetting[0]!='\0' + && zCkin==0 + && !noWarn + ){ + /* There's a versioned setting, and a non-versioned setting. Tell + ** the user about the conflict */ + fossil_warning( + "setting %s has both versioned and non-versioned values: using " + "versioned value from file \"%/.fossil-settings/%s\" (to silence " + "this warning, either create an empty file named " + "\"%/.fossil-settings/%s.no-warn\" in the check-out root, or delete " + "the non-versioned setting with \"fossil unset %s\")", zName, + g.zLocalRoot, zName, g.zLocalRoot, zName, zName + ); + } + + /* Prefer the versioned setting */ + return ( zVersionedSetting!=0 ) ? zVersionedSetting : zNonVersionedSetting; +} + + +/* +** Get and set values from the CONFIG, GLOBAL_CONFIG and VVAR table in the +** repository and local databases. +** +** If no such variable exists, return zDefault. Or, if zName is the name +** of a setting, then the zDefault is ignored and the default value of the +** setting is returned instead. If zName is a versioned setting, then +** versioned value takes priority. +*/ +char *db_get(const char *zName, const char *zDefault){ + char *z = 0; + const Setting *pSetting = db_find_setting(zName, 0); + if( g.repositoryOpen ){ + static Stmt q1; + const char *zRes; + db_static_prepare(&q1, "SELECT value FROM config WHERE name=$n"); + db_bind_text(&q1, "$n", zName); + if( db_step(&q1)==SQLITE_ROW && (zRes = db_column_text(&q1,0))!=0 ){ + z = fossil_strdup(zRes); + } + db_reset(&q1); + } + if( z==0 && g.zConfigDbName ){ + static Stmt q2; + const char *zRes; + db_swap_connections(); + db_static_prepare(&q2, "SELECT value FROM global_config WHERE name=$n"); + db_swap_connections(); + db_bind_text(&q2, "$n", zName); + if( db_step(&q2)==SQLITE_ROW && (zRes = db_column_text(&q2,0))!=0 ){ + z = fossil_strdup(zRes); + } + db_reset(&q2); + } + if( pSetting!=0 && pSetting->versionable ){ + /* This is a versionable setting, try and get the info from a + ** checked-out file */ + char * zZ = z; + z = db_get_versioned(zName, z, 0); + if(zZ != z){ + fossil_free(zZ); + } + } + if( z==0 ){ + if( zDefault==0 && pSetting && pSetting->def[0] ){ + z = fossil_strdup(pSetting->def); + }else{ + z = fossil_strdup(zDefault); + } + } + return z; +} +char *db_get_mtime(const char *zName, const char *zFormat, + const char *zDefault){ + char *z = 0; + if( g.repositoryOpen ){ + z = db_text(0, "SELECT mtime FROM config WHERE name=%Q", zName); + } + if( z==0 ){ + z = fossil_strdup(zDefault); + }else if( zFormat!=0 ){ + z = db_text(0, "SELECT strftime(%Q,%Q,'unixepoch');", zFormat, z); + } + return z; +} +void db_set(const char *zName, const char *zValue, int globalFlag){ + const CmdOrPage *pCmd = 0; + db_assert_protection_off_or_not_sensitive(zName); + if( zValue!=0 && zValue[0]==0 + && dispatch_name_search(zName, CMDFLAG_SETTING, &pCmd)==0 + && (pCmd->eCmdFlags & CMDFLAG_KEEPEMPTY)==0 + ){ + /* Changing a setting to an empty string is the same as unsetting it, + ** unless that setting has the keep-empty flag. */ + db_unset(zName/*works-like:"x"*/, globalFlag); + return; + } + db_unprotect(PROTECT_CONFIG); + db_begin_transaction(); + if( globalFlag ){ + db_swap_connections(); + db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%Q)", + zName, zValue); + db_swap_connections(); + }else{ + db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())", + zName, zValue); + } + if( globalFlag && g.repositoryOpen ){ + db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + } + db_end_transaction(0); + db_protect_pop(); +} +void db_unset(const char *zName, int globalFlag){ + db_begin_transaction(); + db_unprotect(PROTECT_CONFIG); + if( globalFlag ){ + db_swap_connections(); + db_multi_exec("DELETE FROM global_config WHERE name=%Q", zName); + db_swap_connections(); + }else{ + db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + } + if( globalFlag && g.repositoryOpen ){ + db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + } + db_protect_pop(); + db_end_transaction(0); +} +int db_is_global(const char *zName){ + int rc = 0; + if( g.zConfigDbName ){ + db_swap_connections(); + rc = db_exists("SELECT 1 FROM global_config WHERE name=%Q", zName); + db_swap_connections(); + } + return rc; +} +int db_get_int(const char *zName, int dflt){ + int v = dflt; + int rc; + if( g.repositoryOpen ){ + static Stmt q; + db_static_prepare(&q, "SELECT value FROM config WHERE name=$n"); + db_bind_text(&q, "$n", zName); + rc = db_step(&q); + if( rc==SQLITE_ROW ){ + v = db_column_int(&q, 0); + } + db_reset(&q); + }else{ + rc = SQLITE_DONE; + } + if( rc==SQLITE_DONE && g.zConfigDbName ){ + static Stmt q2; + db_swap_connections(); + db_static_prepare(&q2, "SELECT value FROM global_config WHERE name=$n"); + db_swap_connections(); + db_bind_text(&q2, "$n", zName); + if( db_step(&q2)==SQLITE_ROW ){ + v = db_column_int(&q2, 0); + } + db_reset(&q2); + } + return v; +} +i64 db_large_file_size(void){ + /* Return size of the largest file that is not considered oversized */ + return strtoll(db_get("large-file-size","20000000"),0,0); +} +void db_set_int(const char *zName, int value, int globalFlag){ + db_assert_protection_off_or_not_sensitive(zName); + db_unprotect(PROTECT_CONFIG); + if( globalFlag ){ + db_swap_connections(); + db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%d)", + zName, value); + db_swap_connections(); + }else{ + db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%d,now())", + zName, value); + } + if( globalFlag && g.repositoryOpen ){ + db_multi_exec("DELETE FROM config WHERE name=%Q", zName); + } + db_protect_pop(); +} +int db_get_boolean(const char *zName, int dflt){ + char *zVal = db_get(zName, dflt ? "on" : "off"); + if( is_truth(zVal) ){ + dflt = 1; + }else if( is_false(zVal) ){ + dflt = 0; + } + fossil_free(zVal); + return dflt; +} +int db_get_versioned_boolean(const char *zName, int dflt){ + char *zVal = db_get_versioned(zName, 0, 0); + if( zVal==0 ) return dflt; + if( is_truth(zVal) ) return 1; + if( is_false(zVal) ) return 0; + return dflt; +} +char *db_lget(const char *zName, const char *zDefault){ + return db_text(zDefault, + "SELECT value FROM vvar WHERE name=%Q", zName); +} +void db_lset(const char *zName, const char *zValue){ + db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%Q)", zName, zValue); +} +int db_lget_int(const char *zName, int dflt){ + return db_int(dflt, "SELECT value FROM vvar WHERE name=%Q", zName); +} +int db_lget_boolean(const char *zName, int dflt){ + char *zVal = db_lget(zName, dflt ? "on" : "off"); + if( is_truth(zVal) ){ + dflt = 1; + }else if( is_false(zVal) ){ + dflt = 0; + } + fossil_free(zVal); + return dflt; +} +void db_lset_int(const char *zName, int value){ + db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%d)", zName, value); +} + +/* Va-args versions of db_get(), db_set(), and db_unset() +** +** codecheck1.c verifies that the format string for db_set_mprintf() +** and db_unset_mprintf() begins with an ASCII character prefix. We +** don't want that format string to begin with %s or %d as that might +** allow an injection attack to set or overwrite arbitrary settings. +*/ +char *db_get_mprintf(const char *zDefault, const char *zFormat, ...){ + va_list ap; + char *zName; + char *zResult; + va_start(ap, zFormat); + zName = vmprintf(zFormat, ap); + va_end(ap); + zResult = db_get(zName, zDefault); + fossil_free(zName); + return zResult; +} +void db_set_mprintf(const char *zNew, int iGlobal, const char *zFormat, ...){ + va_list ap; + char *zName; + va_start(ap, zFormat); + zName = vmprintf(zFormat, ap); + va_end(ap); + db_set(zName/*works-like:"x"*/, zNew, iGlobal); + fossil_free(zName); +} +void db_unset_mprintf(int iGlobal, const char *zFormat, ...){ + va_list ap; + char *zName; + va_start(ap, zFormat); + zName = vmprintf(zFormat, ap); + va_end(ap); + db_unset(zName/*works-like:"x"*/, iGlobal); + fossil_free(zName); +} + +/* +** Get a setting that is tailored to subsystem. The return value is +** NULL if the setting does not exist, or a string obtained from mprintf() +** if the setting is available. +** +** The actual setting can be a comma-separated list of values of the form: +** +** * VALUE +** * SUBSYSTEM=VALUE +** +** A VALUE without the SUBSYSTEM= prefix is the default. This routine +** returns the VALUE that with the matching SUBSYSTEM, or the default +** VALUE if there is no match. +*/ +char *db_get_for_subsystem(const char *zName, const char *zSubsys){ + int nSubsys; + char *zToFree = 0; + char *zCopy; + char *zNext; + char *zResult = 0; + const char *zSetting = db_get(zName, 0); + if( zSetting==0 ) return 0; + zCopy = zToFree = fossil_strdup(zSetting); + if( zSubsys==0 ) zSubsys = ""; + nSubsys = (int)strlen(zSubsys); + while( zCopy ){ + zNext = strchr(zCopy, ','); + if( zNext ){ + zNext[0] = 0; + do{ zNext++; }while( fossil_isspace(zNext[0]) ); + if( zNext[0]==0 ) zNext = 0; + } + if( strchr(zCopy,'=')==0 ){ + if( zResult==0 ) zResult = zCopy; + }else + if( nSubsys + && strncmp(zCopy, zSubsys, nSubsys)==0 + && zCopy[nSubsys]=='=' + ){ + zResult = &zCopy[nSubsys+1]; + break; + } + zCopy = zNext; + } + if( zResult ) zResult = fossil_strdup(zResult); + fossil_free(zToFree); + return zResult; +} + +#if INTERFACE +/* Manifest generation flags */ +#define MFESTFLG_RAW 0x01 +#define MFESTFLG_UUID 0x02 +#define MFESTFLG_TAGS 0x04 +#endif /* INTERFACE */ + +/* +** Get the manifest setting. For backwards compatibility first check if the +** value is a boolean. If it's not a boolean, treat each character as a flag +** to enable a manifest type. This system puts certain boundary conditions on +** which letters can be used to represent flags (any permutation of flags must +** not be able to fully form one of the boolean values). +** +** "manifest" is a versionable setting. But we do not issue a warning +** if there is a conflict. Instead, the value returned is the value for +** the versioned setting if the versioned setting exists, or the ordinary +** setting otherwise. +** +** The argument zCkin is the specific check-in for which we want the +** manifest setting. +*/ +int db_get_manifest_setting(const char *zCkin){ + int flg; + char *zVal; + + /* Look for the versioned setting first */ + zVal = db_get_versioned("manifest", 0, zCkin); + + if( zVal==0 && g.repositoryOpen ){ + /* No versioned setting, look for the repository setting second */ + zVal = db_text(0, "SELECT value FROM config WHERE name='manifest'"); + } + if( zVal==0 || is_false(zVal) ){ + return 0; + }else if( is_truth(zVal) ){ + return MFESTFLG_RAW|MFESTFLG_UUID; + } + flg = 0; + while( *zVal ){ + switch( *zVal ){ + case 'r': flg |= MFESTFLG_RAW; break; + case 'u': flg |= MFESTFLG_UUID; break; + case 't': flg |= MFESTFLG_TAGS; break; + } + zVal++; + } + return flg; +} + +/* +** COMMAND: test-manifest-setting +** +** Usage: %fossil test-manifest-setting VERSION VERSION ... +** +** Display the value for the "manifest" setting for various versions +** of the repository. +*/ +void test_manfest_setting_cmd(void){ + int i; + db_find_and_open_repository(0, 0); + for(i=2; i<g.argc; i++){ + int m = db_get_manifest_setting(g.argv[i]); + fossil_print("%s:\n", g.argv[i]); + fossil_print(" flags = 0x%02x\n", m); + if( m & MFESTFLG_RAW ){ + fossil_print(" manifest\n"); + } + if( m & MFESTFLG_UUID ){ + fossil_print(" manifest.uuid\n"); + } + if( m & MFESTFLG_TAGS ){ + fossil_print(" manifest.tags\n"); + } + } +} + + +/* +** Record the name of a local repository in the global_config() database. +** The repository filename %s is recorded as an entry with a "name" field +** of the following form: +** +** repo:%s +** +** The value field is set to 1. +** +** If running from a local check-out, also record the root of the check-out +** as follows: +** +** ckout:%s +** +** Where %s is the check-out root. The value is the repository file. +*/ +void db_record_repository_filename(const char *zName){ + char *zRepoSetting; + char *zCkoutSetting; + Blob full; + if( zName==0 ){ + if( !g.localOpen ) return; + zName = db_repository_filename(); + } + file_canonical_name(zName, &full, 0); + (void)filename_collation(); /* Initialize before connection swap */ + db_swap_connections(); + zRepoSetting = mprintf("repo:%q", blob_str(&full)); + + db_unprotect(PROTECT_CONFIG); + db_multi_exec( + "DELETE FROM global_config WHERE name %s = %Q;", + filename_collation(), zRepoSetting + ); + db_multi_exec( + "INSERT OR IGNORE INTO global_config(name,value)" + "VALUES(%Q,1);", + zRepoSetting + ); + db_protect_pop(); + fossil_free(zRepoSetting); + if( g.localOpen && g.zLocalRoot && g.zLocalRoot[0] ){ + Blob localRoot; + file_canonical_name(g.zLocalRoot, &localRoot, 1); + zCkoutSetting = mprintf("ckout:%q", blob_str(&localRoot)); + db_unprotect(PROTECT_CONFIG); + db_multi_exec( + "DELETE FROM global_config WHERE name %s = %Q;", + filename_collation(), zCkoutSetting + ); + db_multi_exec( + "REPLACE INTO global_config(name, value)" + "VALUES(%Q,%Q);", + zCkoutSetting, blob_str(&full) + ); + db_swap_connections(); + db_optional_sql("repository", + "DELETE FROM config WHERE name %s = %Q;", + filename_collation(), zCkoutSetting + ); + db_optional_sql("repository", + "REPLACE INTO config(name,value,mtime)" + "VALUES(%Q,1,now());", + zCkoutSetting + ); + db_protect_pop(); + fossil_free(zCkoutSetting); + blob_reset(&localRoot); + }else{ + db_swap_connections(); + } + blob_reset(&full); +} + +/* +** COMMAND: open +** +** Usage: %fossil open REPOSITORY ?VERSION? ?OPTIONS? +** +** Open a new connection to the repository name REPOSITORY. A check-out +** for the repository is created with its root at the current working +** directory, or in DIR if the "--workdir DIR" is used. If VERSION is +** specified then that version is checked out. Otherwise the most recent +** check-in on the main branch (usually "trunk") is used. +** +** REPOSITORY can be the filename for a repository that already exists on the +** local machine or it can be a URI for a remote repository. If REPOSITORY +** is a URI in one of the formats recognized by the [[clone]] command, the +** remote repo is first cloned, then the clone is opened. The clone will be +** stored in the current directory, or in DIR if the "--repodir DIR" option +** is used. The name of the clone will be taken from the last term of the URI. +** For "http:" and "https:" URIs, you can append an extra term to the end of +** the URI to get any repository name you like. For example: +** +** fossil open https://fossil-scm.org/home/new-name +** +** The base URI for cloning is "https://fossil-scm.org/home". The extra +** "new-name" term means that the cloned repository will be called +** "new-name.fossil". +** +** Options: +** --empty Initialize check-out as being empty, but still connected +** with the local repository. If you commit this check-out, +** it will become a new "initial" commit in the repository. +** -f|--force Continue with the open even if the working directory is +** not empty, or if auto-sync fails. +** --force-missing Force opening a repository with missing content +** -k|--keep Only modify the manifest file(s) +** --nested Allow opening a repository inside an opened check-out +** --nosync Do not auto-sync the repository prior to opening even +** if the autosync setting is on. +** --proxy PROXY Use PROXY as http proxy during sync operation +** --reopen REPO "Reopen" an existing checkout to use a different +** repository file REPO. Useful to reconnect an existing +** checkout to a repository after the original repository +** file is moved. CAUTION: may lose stash and bisect +** history. +** --repodir DIR If REPOSITORY is a URI that will be cloned, store +** the clone in DIR rather than in "." +** --setmtime Set timestamps of all files to match their SCM-side +** times (the timestamp of the last check-in which modified +** them). +** --verbose If passed a URI then this flag is passed on to the clone +** operation, otherwise it has no effect +** --workdir DIR Use DIR as the working directory instead of ".". The DIR +** directory is created if it does not exist. +** +** See also: [[close]], [[clone]] +*/ +void cmd_open(void){ + int emptyFlag; + int keepFlag; + int forceMissingFlag; + int allowNested; + int setmtimeFlag; /* --setmtime. Set mtimes on files */ + int bForce = 0; /* --force. Open even if non-empty dir */ + static char *azNewArgv[] = { 0, "checkout", "--prompt", 0, 0, 0, 0 }; + const char *zWorkDir; /* --workdir value */ + const char *zRepo = 0; /* Name of the repository file */ + const char *zRepoDir = 0; /* --repodir value */ + const char *zReopen = 0; /* --reopen REPOFILE */ + char *zPwd; /* Initial working directory */ + int isUri = 0; /* True if REPOSITORY is a URI */ + int nLocal; /* Number of preexisting files in cwd */ + int bVerbose = 0; /* --verbose option for clone */ + + zReopen = find_option("reopen",0,1); + if( 0!=zReopen ){ + g.argc = 3; + g.argv[2] = (char*)zReopen; + move_repo_cmd(); + return; + } + + url_proxy_options(); + emptyFlag = find_option("empty",0,0)!=0; + keepFlag = find_option("keep","k",0)!=0; + forceMissingFlag = find_option("force-missing",0,0)!=0; + allowNested = find_option("nested",0,0)!=0; + setmtimeFlag = find_option("setmtime",0,0)!=0; + zWorkDir = find_option("workdir",0,1); + zRepoDir = find_option("repodir",0,1); + bForce = find_option("force","f",0)!=0; + if( find_option("nosync",0,0) ) g.fNoSync = 1; + bVerbose = find_option("verbose",0,0)!=0; + zPwd = file_getcwd(0,0); + + /* We should be done with options.. */ + verify_all_options(); + + if( g.argc!=3 && g.argc!=4 ){ + usage("REPOSITORY-FILENAME ?VERSION?"); + } + zRepo = g.argv[2]; + if( sqlite3_strglob("http://*", zRepo)==0 + || sqlite3_strglob("https://*", zRepo)==0 + || sqlite3_strglob("ssh:*", zRepo)==0 + || sqlite3_strglob("file:*", zRepo)==0 + ){ + isUri = 1; + } + + /* If --workdir is specified, change to the requested working directory */ + if( zWorkDir ){ + if( !isUri ){ + zRepo = file_canonical_name_dup(zRepo); + } + if( zRepoDir ){ + zRepoDir = file_canonical_name_dup(zRepoDir); + } + if( file_isdir(zWorkDir, ExtFILE)!=1 ){ + file_mkfolder(zWorkDir, ExtFILE, 0, 0); + if( file_mkdir(zWorkDir, ExtFILE, 0) ){ + fossil_fatal("cannot create directory %s", zWorkDir); + } + } + if( file_chdir(zWorkDir, 0) ){ + fossil_fatal("unable to make %s the working directory", zWorkDir); + } + } + if( keepFlag==0 + && bForce==0 + && (nLocal = file_directory_list(".", 0, 1, 2, 0))>0 + && (nLocal>1 || isUri || !file_in_cwd(zRepo)) + ){ + fossil_fatal("directory %s is not empty\n" + "use the -f (--force) option to override\n" + "or the -k (--keep) option to keep local files unchanged", + file_getcwd(0,0)); + } + + if( db_open_local_v2(0, allowNested) ){ + fossil_fatal("there is already an open tree at %s", g.zLocalRoot); + } + + /* If REPOSITORY looks like a URI, then try to clone it first */ + if( isUri ){ + char *zNewBase; /* Base name of the cloned repository file */ + const char *zUri; /* URI to clone */ + int rc; /* Result code from fossil_system() */ + Blob cmd; /* Clone command to be run */ + char *zCmd; /* String version of the clone command */ + + zUri = zRepo; + zNewBase = url_to_repo_basename(zUri); + if( zNewBase==0 ){ + fossil_fatal("unable to deduce a repository name from the url \"%s\"", + zUri); + } + if( zRepoDir==0 ){ + zRepo = mprintf("%s.fossil", zNewBase); + }else{ + zRepo = mprintf("%s/%s.fossil", zRepoDir, zNewBase); + } + fossil_free(zNewBase); + blob_init(&cmd, 0, 0); + blob_append_escaped_arg(&cmd, g.nameOfExe, 1); + blob_append(&cmd, " clone", -1); + if(0!=bVerbose){ + blob_append(&cmd, " --verbose", -1); + } + blob_append_escaped_arg(&cmd, zUri, 1); + blob_append_escaped_arg(&cmd, zRepo, 1); + zCmd = blob_str(&cmd); + fossil_print("%s\n", zCmd); + if( zWorkDir ) file_chdir(zPwd, 0); + rc = fossil_system(zCmd); + if( rc ){ + fossil_fatal("clone of %s failed", zUri); + } + blob_reset(&cmd); + if( zWorkDir ) file_chdir(zWorkDir, 0); + }else if( zRepoDir ){ + fossil_fatal("the --repodir option only makes sense if the REPOSITORY " + "argument is a URI that begins with http:, https:, ssh:, " + "or file:"); + } + + db_open_config(0,0); + db_open_repository(zRepo); + + /* Figure out which revision to open. */ + if( !emptyFlag ){ + if( g.argc==4 ){ + g.zOpenRevision = g.argv[3]; + }else if( db_exists("SELECT 1 FROM event WHERE type='ci'") ){ + g.zOpenRevision = fossil_strdup(db_main_branch()); + } + if( autosync_loop(SYNC_PULL, !bForce, "open") && !bForce ){ + fossil_fatal("unable to auto-sync the repository"); + } + } + + +#if defined(_WIN32) || defined(__CYGWIN__) +# define LOCALDB_NAME "./_FOSSIL_" +#else +# define LOCALDB_NAME "./.fslckout" +#endif + db_init_database(LOCALDB_NAME, zLocalSchema, zLocalSchemaVmerge, +#ifdef FOSSIL_LOCAL_WAL + "COMMIT; PRAGMA journal_mode=WAL; BEGIN;", +#endif + (char*)0); + db_delete_on_failure(LOCALDB_NAME); + db_open_local(0); + db_lset("repository", zRepo); + db_record_repository_filename(zRepo); + db_set_checkout(0, 0); /* manifest files handled by checkout_cmd */ + azNewArgv[0] = g.argv[0]; + g.argv = azNewArgv; + if( !emptyFlag ){ + g.argc = 3; + if( g.zOpenRevision ){ + azNewArgv[g.argc-1] = g.zOpenRevision; + }else{ + azNewArgv[g.argc-1] = "--latest"; + } + if( keepFlag ){ + azNewArgv[g.argc++] = "--keep"; + } + if( forceMissingFlag ){ + azNewArgv[g.argc++] = "--force-missing"; + } + checkout_cmd(); + } + if( setmtimeFlag ){ + int const vid = db_lget_int("checkout", 0); + if(vid!=0){ + vfile_check_signature(vid, CKSIG_SETMTIME); + } + } + g.argc = 2; + info_cmd(); +} + +/* +** Return true if pSetting has its default value assuming its +** current value is zVal. +*/ +int setting_has_default_value(const Setting *pSetting, const char *zVal){ + if( zVal==0 ) return 1; + if( pSetting->def==0 ) return 0; + if( pSetting->width==0 ){ + return is_false(pSetting->def)==is_false(zVal); + } + if( fossil_strcmp(pSetting->def, zVal)==0 ) return 1; + if( is_false(zVal) && is_false(pSetting->def) ) return 1; + if( is_truth(zVal) && is_truth(pSetting->def) ) return 1; + return 0; +} + +/* +** Print the current value of a setting identified by the pSetting +** pointer. +** +** Only show the value, not the setting name, if valueOnly is true. +** +** Show nothing if bIfChng is true and the setting is not currently set +** or is set to its default value. +*/ +void print_setting(const Setting *pSetting, int valueOnly, int bIfChng){ + Stmt q; + int versioned = 0; + if( pSetting->versionable && g.localOpen ){ + /* Check to see if this is overridden by a versionable settings file */ + Blob versionedPathname; + blob_zero(&versionedPathname); + blob_appendf(&versionedPathname, "%s.fossil-settings/%s", + g.zLocalRoot, pSetting->name); + if( file_size(blob_str(&versionedPathname), ExtFILE)>=0 ){ + versioned = 1; + } + blob_reset(&versionedPathname); + } + if( valueOnly && versioned ){ + const char *zVal = db_get_versioned(pSetting->name, NULL, NULL); + if( !bIfChng || (zVal!=0 && fossil_strcmp(zVal, pSetting->def)!=0) ){ + fossil_print("%s\n", db_get_versioned(pSetting->name, NULL, NULL)); + }else{ + versioned = 0; + } + return; + } + if( g.repositoryOpen ){ + db_prepare(&q, + "SELECT '(local)', value FROM config WHERE name=%Q" + " UNION ALL " + "SELECT '(global)', value FROM global_config WHERE name=%Q", + pSetting->name, pSetting->name + ); + }else{ + db_prepare(&q, + "SELECT '(global)', value FROM global_config WHERE name=%Q", + pSetting->name + ); + } + if( db_step(&q)==SQLITE_ROW ){ + const char *zVal = db_column_text(&q,1); + if( bIfChng && setting_has_default_value(pSetting,zVal) ){ + if( versioned ){ + fossil_print("%-24s (versioned)\n", pSetting->name); + versioned = 0; + } + }else if( valueOnly ){ + fossil_print("%s\n", db_column_text(&q, 1)); + }else{ + const char *zVal = (const char*)db_column_text(&q,1); + const char *zName = (const char*)db_column_text(&q,0); + if( zVal==0 ) zVal = "NULL"; + if( strchr(zVal,'\n')==0 ){ + fossil_print("%-24s %-11s %s\n", pSetting->name, zName, zVal); + }else{ + fossil_print("%-24s %-11s\n", pSetting->name, zName); + while( zVal[0] ){ + const char *zNL = strchr(zVal, '\n'); + if( zNL==0 ){ + fossil_print(" %s\n", zVal); + break; + }else{ + int n = (int)(zNL - zVal); + while( n>0 && fossil_isspace(zVal[n-1]) ){ n--; } + fossil_print(" %.*s\n", n, zVal); + zVal = zNL+1; + } + } + } + } + }else if( bIfChng ){ + /* Display nothing */ + versioned = 0; + }else if( valueOnly ){ + fossil_print("\n"); + }else{ + fossil_print("%-24s\n", pSetting->name); + } + if( versioned ){ + fossil_print(" (overridden by contents of file .fossil-settings/%s)\n", + pSetting->name); + } + db_finalize(&q); +} + +#if INTERFACE +/* +** Define all settings, which can be controlled via the set/unset +** command. +** +** var is the name of the internal configuration name for db_(un)set. +** If var is 0, the settings name is used. +** +** width is the length for the edit field on the behavior page, 0 is +** used for on/off checkboxes. A negative value indicates that the +** page should not render this setting. Such values may be rendered +** separately/manually on another page, e.g., /setup_access, and are +** exposed via the CLI settings command. +** +** The behaviour page doesn't use a special layout. It lists all +** set-commands and displays the 'set'-help as info. +*/ +struct Setting { + const char *name; /* Name of the setting */ + const char *var; /* Internal variable name used by db_set() */ + int width; /* Width of display. 0 for boolean values and + ** negative for values which should not appear + ** on the /setup_settings page. */ + char versionable; /* Is this setting versionable? */ + char forceTextArea; /* Force using a text area for display? */ + char sensitive; /* True if this a security-sensitive setting */ + const char *def; /* Default value */ +}; + +#endif /* INTERFACE */ + +/* +** SETTING: access-log boolean default=on +** +** When the access-log setting is enabled, all login attempts (successful +** and unsuccessful) on the web interface are recorded in the "access" table +** of the repository. +*/ +/* +** SETTING: admin-log boolean default=on +** +** When the admin-log setting is enabled, configuration changes are recorded +** in the "admin_log" table of the repository. +*/ +/* +** SETTING: allow-symlinks boolean default=off sensitive +** +** When allow-symlinks is OFF, Fossil does not see symbolic links +** (a.k.a "symlinks") on disk as a separate class of object. Instead Fossil +** sees the object that the symlink points to. Fossil will only manage files +** and directories, not symlinks. When a symlink is added to a repository, +** the object that the symlink points to is added, not the symlink itself. +** +** When allow-symlinks is ON, Fossil sees symlinks on disk as a separate +** object class that is distinct from files and directories. When a symlink +** is added to a repository, Fossil stores the target filename. In other +** words, Fossil stores the symlink itself, not the object that the symlink +** points to. +** +** Symlinks are not cross-platform. They are not available on all +** operating systems and file systems. Hence the allow-symlinks setting is +** OFF by default, for portability. +*/ +/* +** SETTING: auto-captcha boolean default=on variable=autocaptcha +** If enabled, the /login page provides a button that will automatically +** fill in the captcha password. This makes things easier for human users, +** at the expense of also making logins easier for malicious robots. +*/ +/* +** SETTING: auto-hyperlink width=16 default=1 +** +** If non-zero, enable hyperlinks on web pages even for users that lack +** the "h" privilege as long as the UserAgent string in the HTTP request +** (The HTTP_USER_AGENT cgi variable) looks like it comes from a human and +** not a robot. Details depend on the value of the setting. +** +** (0) Off: No adjustments are made to the 'h' privilege based on +** the user agent. +** +** (1) UserAgent and Javascript: The the href= values of hyperlinks +** initially point to /honeypot and are changed to point to the +** correct target by javascript that runs after the page loads. +** The auto-hyperlink-delay and auto-hyperlink-mouseover settings +** influence that javascript. +** +** (2) UserAgent only: If the HTTP_USER_AGENT looks human +** then generate hyperlinks, otherwise do not. +** +** Better robot exclusion is obtained when this setting is 1 versus 2. +** However, a value of 1 causes the visited/unvisited colors of hyperlinks +** to stop working on Safari-derived web browsers. When this setting is 2, +** the hyperlinks work better on Safari, but more robots are able to sneak +** in. +*/ +/* +** SETTING: auto-hyperlink-delay width=16 default=0 +** +** When the auto-hyperlink setting is 1, the javascript that runs to set +** the href= attributes of hyperlinks delays by this many milliseconds +** after the page load. Suggested values: 50 to 200. +*/ +/* +** SETTING: auto-hyperlink-mouseover boolean default=off +** +** When the auto-hyperlink setting is 1 and this setting is on, the +** javascript that runs to set the href= attributes of hyperlinks waits +** until either a mousedown or mousemove event is seen. This helps +** to distinguish real users from robots. For maximum robot defense, +** the recommended setting is ON. +*/ +/* +** SETTING: auto-shun boolean default=on +** If enabled, automatically pull the shunning list +** from a server to which the client autosyncs. +*/ +/* +** SETTING: autosync width=16 default=on +** This setting determines when autosync occurs. The setting is a +** string that provides a lot of flexibility for determining when and +** when not to autosync. Examples: +** +** on Always autosync for command where autosync +** makes sense ("commit", "merge", "open", "update") +** +** off Never autosync. +** +** pullonly Only to pull autosyncs +** +** all Sync with all remotes +** +** on,open=off Autosync for most commands, but not for "open" +** +** off,commit=pullonly Do not autosync, except do a pull before each +** "commit", presumably to avoid undesirable +** forks. +** +** The syntax is a comma-separated list of VALUE and COMMAND=VALUE entries. +** A plain VALUE entry is the default that is used if no COMMAND matches. +** Otherwise, the VALUE of the matching command is used. +** +** The "all" value is special in that it applies to the "sync" command in +** addition to "commit", "merge", "open", and "update". +*/ +/* +** SETTING: autosync-tries width=16 default=1 +** If autosync is enabled setting this to a value greater +** than zero will cause autosync to try no more than this +** number of attempts if there is a sync failure. +*/ +/* +** SETTING: backoffice-nodelay boolean default=off +** If backoffice-nodelay is true, then the backoffice processing +** will never invoke sleep(). If it has nothing useful to do, +** it simply exits. +*/ +/* +** SETTING: backoffice-disable boolean default=off +** If backoffice-disable is true, then the automatic backoffice +** processing is disabled. Automatic backoffice processing is the +** backoffice work that normally runs after each web page is +** rendered. Backoffice processing that is triggered by the +** "fossil backoffice" command is unaffected by this setting. +** +** Backoffice processing does things such as delivering +** email notifications. So if this setting is true, and if +** there is no cron job periodically running "fossil backoffice", +** email notifications and other work normally done by the +** backoffice will not occur. +*/ +/* +** SETTING: backoffice-logfile width=40 sensitive +** If backoffice-logfile is not an empty string and is a valid +** filename, then a one-line message is appended to that file +** every time the backoffice runs. This can be used for debugging, +** to ensure that backoffice is running appropriately. +*/ +/* +** SETTING: binary-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files +** that should be treated as "binary" for committing and merging +** purposes. Example: *.jpg,*.png The parsing rules are complex; +** see https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +*/ +#if defined(_WIN32)||defined(__CYGWIN__)||defined(__DARWIN__) +/* +** SETTING: case-sensitive boolean default=off +** If TRUE, the files whose names differ only in case +** are considered distinct. If FALSE files whose names +** differ only in case are the same file. Defaults to +** TRUE for unix and FALSE for Cygwin, Mac and Windows. +*/ +#endif +#if !(defined(_WIN32)||defined(__CYGWIN__)||defined(__DARWIN__)) +/* +** SETTING: case-sensitive boolean default=on +** If TRUE, the files whose names differ only in case +** are considered distinct. If FALSE files whose names +** differ only in case are the same file. Defaults to +** TRUE for unix and FALSE for Cygwin, Mac and Windows. +*/ +#endif +/* +** SETTING: clean-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files +** that the "clean" command will delete without prompting or allowing +** undo. Example: *.a,*.o,*.so The parsing rules are complex; +** see https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +*/ +/* +** SETTING: clearsign boolean default=off +** When enabled, fossil will attempt to sign all commits +** with gpg or ssh. When disabled, commits will be unsigned. +*/ +/* +** SETTING: comment-format width=16 default=1 +** Set the algorithm for printing timeline comments to the console. +** +** Possible values are: +** 1 Use the original comment printing algorithm: +** * Leading and trailing whitespace is removed +** * Internal whitespace is converted into a single space (0x20) +** * Line breaks occurs at whitespace or hyphens if possible +** This is the recommended value and the default. +** +** Or a bitwise combination of the following flags: +** 2 Trim leading and trailing CR and LF characters. +** 4 Trim leading and trailing white space characters. +** 8 Attempt to break lines on word boundaries. +** 16 Break lines before the original comment embedded in other text. +** +** Note: To preserve line breaks and/or other whitespace within comment text, +** make this setting some integer value that omits the "1" bit. +*/ +/* +** SETTING: crlf-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files +** in which it is allowed to have CR, CR+LF or mixed line endings, +** suppressing Fossil's normal warning about this. Set it to "*" to +** disable CR+LF checking entirely. Example: *.md,*.txt +** The crnl-glob setting is a compatibility alias. +*/ +/* +** SETTING: crnl-glob width=40 versionable block-text +** This is an alias for the crlf-glob setting. +*/ +/* +** SETTING: default-perms width=16 default=u sensitive keep-empty +** Permissions given automatically to new users. For more +** information on permissions see the Users page in Server +** Administration of the HTTP UI. +*/ +/* +** SETTING: diff-binary boolean default=on +** If enabled, permit files that may be binary +** or that match the "binary-glob" setting to be used with +** external diff programs. If disabled, skip these files. +*/ +/* +** SETTING: diff-command width=40 sensitive +** The value is an external command to run when performing a diff. +** If undefined, the internal text diff will be used. +*/ +/* +** SETTING: dont-commit boolean default=off +** If enabled, prevent committing to this repository, as an extra precaution +** against accidentally checking in to a repository intended to be read-only. +*/ +/* +** SETTING: dont-push boolean default=off +** If enabled, prevent this repository from pushing from client to +** server. This can be used as an extra precaution to prevent +** accidental pushes to a public server from a private clone. +*/ +/* +** SETTING: dotfiles boolean versionable default=off +** If enabled, include --dotfiles option for all compatible commands. +*/ +/* +** SETTING: editor width=32 sensitive +** The value is an external command that will launch the +** text editor command used for check-in comments. +** +** If this value is not set, then environment variables VISUAL and +** EDITOR are consulted, in that order. If neither of those are set, +** then a search is made for common text editors, including +** "notepad", "nano", "pico", "jove", "edit", "vi", "vim", and "ed". +** +** If this setting is false ("off", "no", "false", or "0") then no +** text editor is used. +*/ +/* +** SETTING: empty-dirs width=40 versionable block-text +** The value is a list of pathnames parsed according to the same rules as +** the *-glob settings. On update and checkout commands, if no directory +** exists with that name, an empty directory will be created, even if +** it must create one or more parent directories. +*/ +/* +** SETTING: encoding-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files that +** the "commit" command will ignore when issuing warnings about text files +** that may use another encoding than ASCII or UTF-8. Set to "*" to disable +** encoding checking. Example: *.md,*.txt The parsing rules are complex; +** see https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +*/ +#if defined(FOSSIL_ENABLE_EXEC_REL_PATHS) +/* +** SETTING: exec-rel-paths boolean default=on +** When executing certain external commands (e.g. diff and +** gdiff), use relative paths. +*/ +#endif +#if !defined(FOSSIL_ENABLE_EXEC_REL_PATHS) +/* +** SETTING: exec-rel-paths boolean default=off +** When executing certain external commands (e.g. diff and +** gdiff), use relative paths. +*/ +#endif + +/* +** SETTING: fileedit-glob width=40 block-text +** The VALUE of this setting is a list of GLOB patterns matching files +** which are allowed to be edited using the /fileedit page. An empty list +** suppresses the feature. Example: *.md,*.txt The parsing rules are +** complex; see https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +** Note that /fileedit cannot edit binary files, so the list should not +** contain any globs for, e.g., images or PDFs. +*/ +/* +** SETTING: forbid-delta-manifests boolean default=off +** If enabled on a client, new delta manifests are prohibited on +** commits. If enabled on a server, whenever a client attempts +** to obtain a check-in lock during auto-sync, the server will +** send the "pragma avoid-delta-manifests" statement in its reply, +** which will cause the client to avoid generating a delta +** manifest. +*/ +/* +** SETTING: gdiff-command width=40 sensitive +** The value is an external command to run when performing a graphical +** diff. If undefined, a --tk diff is done if commands "tclsh" and "wish" +** are on PATH, or a --by diff is done if "tclsh" or "wish" are unavailable. +*/ +/* +** SETTING: gmerge-command width=40 sensitive +** The value is a graphical merge conflict resolver command operating +** on four files. Examples: +** +** kdiff3 "%baseline" "%original" "%merge" -o "%output" +** xxdiff "%original" "%baseline" "%merge" -M "%output" +** meld "%baseline" "%original" "%merge" --output "%output" +*/ +/* +** SETTING: hash-digits width=5 default=10 +** The number of hexadecimal digits of the SHA3 hash to display. +*/ +/* +** SETTING: http-port width=16 default=8080 +** The default TCP/IP port number to use by the "server" +** and "ui" commands. +*/ +/* +** SETTING: https-login boolean default=off +** If true, then the Fossil web server will redirect unencrypted +** login screen requests to HTTPS. +*/ +/* +** SETTING: ignore-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files that +** the "add", "addremove", "clean", and "extras" commands will ignore. +** Example: *.log,notes.txt The parsing rules are complex; see +** https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +*/ +/* +** SETTING: keep-glob width=40 versionable block-text +** The VALUE of this setting is a list of GLOB patterns matching files that +** the "clean" command must not delete. Example: build/precious.exe +** The parsing rules are complex; see +** https://fossil-scm.org/home/doc/trunk/www/globs.md#syntax +*/ +/* +** SETTING: localauth boolean default=off +** If enabled, require that HTTP connections from the loopback +** address (127.0.0.1) be authenticated by password. If false, +** some HTTP requests might be granted full "Setup" user +** privileges without having to present login credentials. +** This mechanism allows the "fossil ui" command to provide +** full access to the repository without requiring the user to +** log in first. +** +** In order for full "Setup" privilege to be granted without a +** login, the following conditions must be met: +** +** (1) This setting ("localauth") must be off +** (2) The HTTP request arrive over the loopback TCP/IP +** address (127.0.01) or else via SSH. +** (3) The request must be HTTP, not HTTPS. (This +** restriction is designed to help prevent accidentally +** providing "Setup" privileges to requests arriving +** over a reverse proxy.) +** (4) The command that launched the fossil server must be +** one of the following: +** (a) "fossil ui" +** (b) "fossil server" with the --localauth option +** (c) "fossil http" with the --localauth option +** (d) CGI with the "localauth" setting in the cgi script. +** +** For maximum security, set "localauth" to 1. However, because +** of the other restrictions (2) through (4), it should be safe +** to leave "localauth" set to 0 in most installations, and +** especially on cloned repositories on workstations. Leaving +** "localauth" at 0 makes the "fossil ui" command more convenient +** to use. +*/ +/* +** SETTING: lock-timeout width=25 default=60 +** This is the number of seconds that a check-in lock will be held on +** the server before the lock expires. The default is a 60-second delay. +** Set this value to zero to disable the check-in lock mechanism. +** +** This value should be set on the server to which users auto-sync +** their work. This setting has no effect on client repositories. The +** check-in lock mechanism is only effective if all users are auto-syncing +** to the same server. +** +** Check-in locks are an advisory mechanism designed to help prevent +** accidental forks due to a check-in race in installations where many +** users are committing to the same branch and auto-sync is enabled. +** As forks are harmless, there is no danger in disabling this mechanism. +** However, keeping check-in locks turned on can help prevent unnecessary +** confusion. +*/ +/* +** SETTING: main-branch width=40 default=trunk +** The value is the primary branch for the project. +*/ +/* +** SETTING: manifest width=5 versionable +** If enabled, automatically create files "manifest" and "manifest.uuid" +** in every check-out. +** +** Optionally use combinations of characters 'r' for "manifest", +** 'u' for "manifest.uuid" and 't' for "manifest.tags". The SQLite +** and Fossil repositories both require manifests. +*/ +/* +** SETTING: max-loadavg width=25 default=0.0 +** Some CPU-intensive web pages (ex: /zip, /tarball, /blame) +** are disallowed if the system load average goes above this +** value. "0.0" means no limit. This only works on unix. +** Only local settings of this value make a difference since +** when running as a web-server, Fossil does not open the +** global configuration database. +*/ +/* +** SETTING: max-upload width=25 default=250000 +** A limit on the size of uplink HTTP requests. +*/ +/* +** SETTING: mimetypes width=40 versionable block-text +** A list of file extension-to-mimetype mappings, one per line. e.g. +** "foo application/x-foo". File extensions are compared +** case-insensitively in the order listed in this setting. A leading +** '.' on file extensions is permitted but not required. +*/ +/* +** SETTING: mtime-changes boolean default=on +** Use file modification times (mtimes) to detect when +** files have been modified. If disabled, all managed files +** are hashed to detect changes, which can be slow for large +** projects. +*/ +/* +** SETTING: mv-rm-files boolean default=off +** If enabled, the "mv" and "rename" commands will also move +** the associated files within the check-out -AND- the "rm" +** and "delete" commands will also remove the associated +** files from within the check-out. +*/ +/* +** SETTING: pgp-command width=40 sensitive +** Command used to clear-sign manifests at check-in. +** Default value is "gpg --clearsign -o". +** For SSH, use e.g. "ssh-keygen -q -Y sign -n fossilscm -f ~/.ssh/id_ed25519" +*/ +/* +** SETTING: proxy width=32 default=system +** URL of the HTTP proxy. If undefined or "system", the "http_proxy" +** environment variable is consulted. If "off", a direct HTTP connection is +** used. +*/ +/* +** SETTING: redirect-to-https default=0 width=2 +** Specifies whether or not to redirect unencrypted "http://" requests to +** encrypted "https://" URIs. A value of 0 (the default) means do not +** redirect, 1 means to redirect only the /login page, and 2 +** means to always redirect. +** +** For security, a value of 2 is recommended. The default value is 0 +** because not all sites are TLS-capable. But you should definitely enable +** TLS and change this setting to 2 for all public-facing repositories. +*/ +/* +** SETTING: relative-paths boolean default=on +** When showing changes and extras, report paths relative +** to the current working directory. +*/ +/* +** SETTING: repo-cksum boolean default=on +** Compute checksums over all files in each check-out as a double-check +** of correctness. Disable this on large repositories for a performance +** improvement. +*/ +/* +** SETTING: repolist-skin width=2 default=0 +** If non-zero then use this repository as the skin for a repository list +** such as created by the one of: +** +** 1) fossil server DIRECTORY --repolist +** 2) fossil ui DIRECTORY --repolist +** 3) fossil http DIRECTORY --repolist +** 4) (The "repolist" option in a CGI script) +** 5) fossil all ui +** 6) fossil all server +** +** All repositories are searched (in lexicographical order) and the first +** repository with a non-zero "repolist-skin" value is used as the skin +** for the repository list page. If none of the repositories on the list +** have a non-zero "repolist-skin" setting then the repository list is +** displayed using unadorned HTML ("skinless"), with the page title taken +** from the FOSSIL_REPOLIST_TITLE environment variable. +** +** If repolist-skin has a value of 2, then the repository is omitted from +** the list in use cases 1 through 4, but not for 5 and 6. +*/ +/* +** SETTING: self-pw-reset boolean default=off sensitive +** Allow users to request that an email containing a hyperlink +** to the /resetpw page be sent to their email address of record, +** thus allowing forgetful users to reset their forgotten passwords +** without administrator involvement. +*/ +/* +** SETTING: self-register boolean default=off sensitive +** Allow users to register themselves through the HTTP UI. +** This is useful if you want to see other names than +** "Anonymous" in e.g. ticketing system. On the other hand +** users can not be deleted. +*/ +/* +** SETTING: ssh-command width=40 sensitive +** The command used to talk to a remote machine with the "ssh://" protocol. +*/ + +/* +** SETTING: ssl-ca-location width=40 sensitive +** The full pathname to a file containing PEM encoded +** CA root certificates, or a directory of certificates +** with filenames formed from the certificate hashes as +** required by OpenSSL. +** +** If set, this will override the OS default list of +** OpenSSL CAs. If unset, the default list will be used. +** Some platforms may add additional certificates. +** Checking your platform behaviour is required if the +** exact contents of the CA root is critical for your +** application. +** +** This setting is overridden by environment variables +** SSL_CERT_FILE and SSL_CERT_DIR. +*/ +/* +** SETTING: ssl-identity width=40 sensitive +** The full pathname to a file containing a certificate +** and private key in PEM format. Create by concatenating +** the certificate and private key files. +** +** This identity will be presented to SSL servers to +** authenticate this client, in addition to the normal +** password authentication. +*/ +#ifdef FOSSIL_ENABLE_TCL +/* +** SETTING: tcl boolean default=off sensitive +** If enabled Tcl integration commands will be added to the TH1 +** interpreter, allowing arbitrary Tcl expressions and +** scripts to be evaluated from TH1. Additionally, the Tcl +** interpreter will be able to evaluate arbitrary TH1 +** expressions and scripts. +*/ +/* +** SETTING: tcl-setup width=40 block-text sensitive +** This is the setup script to be evaluated after creating +** and initializing the Tcl interpreter. By default, this +** is empty and no extra setup is performed. +*/ +#endif /* FOSSIL_ENABLE_TCL */ +/* +** SETTING: tclsh width=80 default=tclsh sensitive +** Name of the external TCL interpreter used for such things +** as running the GUI diff viewer launched by the --tk option +** of the various "diff" commands. +*/ +#ifdef FOSSIL_ENABLE_TH1_DOCS +/* +** SETTING: th1-docs boolean default=off sensitive +** If enabled, this allows embedded documentation files to contain +** arbitrary TH1 scripts that are evaluated on the server. If native +** Tcl integration is also enabled, this setting has the +** potential to allow anybody with check-in privileges to +** do almost anything that the associated operating system +** user account could do. Extreme caution should be used +** when enabling this setting. +*/ +#endif +#ifdef FOSSIL_ENABLE_TH1_HOOKS +/* +** SETTING: th1-hooks boolean default=off +** If enabled, special TH1 commands will be called before and +** after any Fossil command or web page. +*/ +#endif +/* +** SETTING: th1-setup width=40 block-text sensitive +** This is the setup script to be evaluated after creating +** and initializing the TH1 interpreter. By default, this +** is empty and no extra setup is performed. +*/ +/* +** SETTING: th1-uri-regexp width=40 block-text +** Specify which URI's are allowed in HTTP requests from +** TH1 scripts. If empty, no HTTP requests are allowed +** whatsoever. +*/ +/* +** SETTING: default-csp width=40 block-text keep-empty +** +** The text of the Content Security Policy that is included +** in the Content-Security-Policy: header field of the HTTP +** reply and in the default HTML <head> section that is added when the +** skin header does not specify a <head> section. The text "$nonce" +** is replaced by the random nonce that is created for each web page. +** +** If this setting is an empty string or is omitted, then +** the following default Content Security Policy is used: +** +** default-src 'self' data:; +** script-src 'self' 'nonce-$nonce'; +** style-src 'self' 'unsafe-inline'; +** img-src * data:; +** +** The default CSP is recommended. The main reason to change +** this setting would be to add CDNs from which it is safe to +** load additional content. +*/ +/* +** SETTING: uv-sync boolean default=off +** If true, automatically send unversioned files as part +** of a "fossil clone" or "fossil sync" command. The +** default is false, in which case the -u option is +** needed to clone or sync unversioned files. +*/ +/* +** SETTING: web-browser width=30 sensitive +** A shell command used to launch your preferred +** web browser when given a URL as an argument. +** Defaults to "start" on windows, "open" on Mac, +** and "firefox" on Unix. +*/ +/* +** SETTING: large-file-size width=10 default=200000000 +** Fossil considers any file whose size is greater than this value +** to be a "large file". Fossil might issue warnings if you try to +** "add" or "commit" a "large file". Set this value to 0 or less +** to disable all such warnings. +*/ + +/* +** Look up a control setting by its name. Return a pointer to the Setting +** object, or NULL if there is no such setting. +** +** If allowPrefix is true, then the Setting returned is the first one for +** which zName is a prefix of the Setting name. +*/ +Setting *db_find_setting(const char *zName, int allowPrefix){ + int lwr, mid, upr, c; + int n = (int)strlen(zName) + !allowPrefix; + int nSetting; + const Setting *aSetting = setting_info(&nSetting); + lwr = 0; + upr = nSetting - 1; + while( upr>=lwr ){ + mid = (upr+lwr)/2; + c = fossil_strncmp(zName, aSetting[mid].name, n); + if( c<0 ){ + upr = mid - 1; + }else if( c>0 ){ + lwr = mid + 1; + }else{ + if( allowPrefix ){ + while( mid>lwr && fossil_strncmp(zName, aSetting[mid-1].name, n)==0 ){ + mid--; + } + } + return (Setting*)&aSetting[mid]; + } + } + return 0; +} + +/* +** COMMAND: settings +** COMMAND: unset* +** +** Usage: %fossil settings ?SETTING? ?VALUE? ?OPTIONS? +** or: %fossil unset SETTING ?OPTIONS? +** +** The "settings" command with no arguments lists all settings and their +** values. With just a SETTING name it shows the current value of that setting. +** With a VALUE argument it changes the property for the current repository. +** +** Settings marked as versionable are overridden by the contents of the +** file named .fossil-settings/PROPERTY in the check-out root, if that +** file exists. +** +** The "unset" command clears a setting. +** +** Settings can have both a "local" repository-only value and "global" value +** that applies to all repositories. The local values are stored in the +** "config" table of the repository and the global values are stored in the +** configuration database. If both a local and a global value exists for a +** setting, the local value takes precedence. This command normally operates +** on the local settings. Use the --global option to change global settings. +** +** Options: +** --changed Only show settings if the value differs from the default +** --exact Only consider exact name matches +** --global Set or unset the given property globally instead of +** setting or unsetting it for the open repository only +** --value Only show the value of a given property (implies --exact) +** +** See also: [[configuration]] +*/ +void setting_cmd(void){ + int i; + int globalFlag = find_option("global","g",0)!=0; + int bIfChng = find_option("changed",0,0)!=0; + int exactFlag = find_option("exact",0,0)!=0; + int valueFlag = find_option("value",0,0)!=0; + /* Undocumented "--test-for-subsystem SUBSYS" option used to test + ** the db_get_for_subsystem() interface: */ + const char *zSubsys = find_option("test-for-subsystem",0,1); + int unsetFlag = g.argv[1][0]=='u'; + int nSetting; + const Setting *aSetting = setting_info(&nSetting); + find_repository_option(); + verify_all_options(); + db_open_config(1, 0); + if( !globalFlag ){ + db_find_and_open_repository(OPEN_ANY_SCHEMA | OPEN_OK_NOT_FOUND, 0); + } + if( !g.repositoryOpen ){ + globalFlag = 1; + } + if( unsetFlag && g.argc!=3 ){ + usage("PROPERTY ?-global?"); + } + if( valueFlag ){ + if( g.argc!=3 ){ + fossil_fatal("--value is only supported when qurying a given property"); + } + } + + if( g.argc==2 ){ + for(i=0; i<nSetting; i++){ + print_setting(&aSetting[i], 0, bIfChng); + } + }else if( g.argc==3 || g.argc==4 ){ + const char *zName = g.argv[2]; + int n = (int)strlen(zName); + const Setting *pSetting = db_find_setting(zName, !exactFlag); + if( pSetting==0 ){ + fossil_fatal("no such setting: %s", zName); + } + if( globalFlag && fossil_strcmp(pSetting->name, "manifest")==0 ){ + fossil_fatal("cannot set 'manifest' globally"); + } + if( unsetFlag || g.argc==4 ){ + int isManifest = fossil_strcmp(pSetting->name, "manifest")==0; + if( n!=(int)strlen(pSetting[0].name) && pSetting[1].name && + fossil_strncmp(pSetting[1].name, zName, n)==0 ){ + Blob x; + int i; + blob_init(&x,0,0); + for(i=0; pSetting[i].name; i++){ + if( fossil_strncmp(pSetting[i].name,zName,n)!=0 ) break; + blob_appendf(&x, " %s", pSetting[i].name); + } + fossil_fatal("ambiguous setting \"%s\" - might be:%s", + zName, blob_str(&x)); + } + if( globalFlag && isManifest ){ + fossil_fatal("cannot set 'manifest' globally"); + } + if( unsetFlag ){ + db_unset(pSetting->name/*works-like:"x"*/, globalFlag); + }else{ + db_protect_only(PROTECT_NONE); + db_set(pSetting->name/*works-like:"x"*/, g.argv[3], globalFlag); + db_protect_pop(); + } + if( isManifest && g.localOpen ){ + manifest_to_disk(db_lget_int("checkout", 0)); + } + }else{ + while( pSetting->name ){ + if( exactFlag ){ + if( fossil_strcmp(pSetting->name,zName)!=0 ) break; + }else{ + if( fossil_strncmp(pSetting->name,zName,n)!=0 ) break; + } + if( zSubsys ){ + char *zValue = db_get_for_subsystem(pSetting->name, zSubsys); + fossil_print("%s (subsystem %s) ->", pSetting->name, zSubsys); + if( zValue ){ + fossil_print(" [%s]", zValue); + fossil_free(zValue); + } + fossil_print("\n"); + }else{ + print_setting(pSetting, valueFlag, bIfChng); + } + pSetting++; + } + } + }else{ + usage("?PROPERTY? ?VALUE? ?-global?"); + } +} + +/* +** The input in a timespan measured in days. Return a string which +** describes that timespan in units of seconds, minutes, hours, days, +** or years, depending on its duration. +*/ +char *db_timespan_name(double rSpan){ + if( rSpan<0 ) rSpan = -rSpan; + rSpan *= 24.0*3600.0; /* Convert units to seconds */ + if( rSpan<120.0 ){ + return sqlite3_mprintf("%.1f seconds", rSpan); + } + rSpan /= 60.0; /* Convert units to minutes */ + if( rSpan<90.0 ){ + return sqlite3_mprintf("%.1f minutes", rSpan); + } + rSpan /= 60.0; /* Convert units to hours */ + if( rSpan<=48.0 ){ + return sqlite3_mprintf("%.1f hours", rSpan); + } + rSpan /= 24.0; /* Convert units to days */ + if( rSpan<=365.0 ){ + return sqlite3_mprintf("%.1f days", rSpan); + } + rSpan /= 356.24; /* Convert units to years */ + return sqlite3_mprintf("%.1f years", rSpan); +} + +/* +** COMMAND: test-timespan +** +** Usage: %fossil test-timespan TIMESTAMP +** +** Print the approximate span of time from now to TIMESTAMP. +*/ +void test_timespan_cmd(void){ + double rDiff; + if( g.argc!=3 ) usage("TIMESTAMP"); + sqlite3_open(":memory:", &g.db); + rDiff = db_double(0.0, "SELECT julianday('now') - julianday(%Q)", g.argv[2]); + fossil_print("Time differences: %s\n", db_timespan_name(rDiff)); + sqlite3_close(g.db); + g.db = 0; + g.repositoryOpen = 0; + g.localOpen = 0; +} + +/* +** COMMAND: test-without-rowid +** +** Usage: %fossil test-without-rowid FILENAME... +** +** Change the Fossil repository FILENAME to make use of the WITHOUT ROWID +** optimization. FILENAME can also be the configuration database file +** (~/.fossil or ~/.config/fossil.db) or a local .fslckout or _FOSSIL_ file. +** +** The purpose of this command is for testing the WITHOUT ROWID capabilities +** of SQLite. There is no big advantage to using WITHOUT ROWID in Fossil. +** +** Options: +** -n|--dry-run No changes. Just print what would happen. +*/ +void test_without_rowid(void){ + int i, j; + Stmt q; + Blob allSql; + int dryRun = find_option("dry-run", "n", 0)!=0; + for(i=2; i<g.argc; i++){ + db_open_or_attach(g.argv[i], "main"); + blob_init(&allSql, "BEGIN;\n", -1); + db_prepare(&q, + "SELECT name, sql FROM main.sqlite_schema " + " WHERE type='table' AND sql NOT LIKE '%%WITHOUT ROWID%%'" + " AND name IN ('global_config','shun','concealed','config'," + " 'plink','tagxref','backlink','vcache');" + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zTName = db_column_text(&q, 0); + const char *zOrigSql = db_column_text(&q, 1); + Blob newSql; + blob_init(&newSql, 0, 0); + for(j=0; zOrigSql[j]; j++){ + if( fossil_strnicmp(zOrigSql+j,"unique",6)==0 ){ + blob_append(&newSql, zOrigSql, j); + blob_append(&newSql, "PRIMARY KEY", -1); + zOrigSql += j+6; + j = -1; + } + } + blob_append(&newSql, zOrigSql, -1); + blob_append_sql(&allSql, + "ALTER TABLE \"%w\" RENAME TO \"x_%w\";\n" + "%s WITHOUT ROWID;\n" + "INSERT INTO \"%w\" SELECT * FROM \"x_%w\";\n" + "DROP TABLE \"x_%w\";\n", + zTName, zTName, blob_sql_text(&newSql), zTName, zTName, zTName + ); + fossil_print("Converting table %s of %s to WITHOUT ROWID.\n", + zTName, g.argv[i]); + blob_reset(&newSql); + } + blob_append_sql(&allSql, "COMMIT;\n"); + db_finalize(&q); + if( dryRun ){ + fossil_print("SQL that would have been evaluated:\n"); + fossil_print("%.78c\n", '-'); + fossil_print("%s", blob_sql_text(&allSql)); + }else{ + db_multi_exec("%s", blob_sql_text(&allSql)); + } + blob_reset(&allSql); + db_close(1); + } +} + +/* +** Make sure the adminlog table exists. Create it if it does not +*/ +void create_admin_log_table(void){ + static int once = 0; + if( once ) return; + if( !db_table_exists("repository","admin_log") ){ + once = 1; + db_multi_exec( + "CREATE TABLE repository.admin_log(\n" + " id INTEGER PRIMARY KEY,\n" + " time INTEGER, -- Seconds since 1970\n" + " page TEXT, -- path of page\n" + " who TEXT, -- User who made the change\n" + " what TEXT -- What changed\n" + ")" + ); + } +} + +/* +** Write a message into the admin_event table, if admin logging is +** enabled via the admin-log configuration option. +*/ +void admin_log(const char *zFormat, ...){ + Blob what = empty_blob; + va_list ap; + if( !db_get_boolean("admin-log", 0) ){ + /* Potential leak here (on %z params) but + the alternative is to let blob_vappendf() + do it below. */ + return; + } + create_admin_log_table(); + va_start(ap,zFormat); + blob_vappendf( &what, zFormat, ap ); + va_end(ap); + db_multi_exec("INSERT INTO admin_log(time,page,who,what)" + " VALUES(now(), %Q, %Q, %B)", + g.zPath, g.zLogin, &what); + blob_reset(&what); +} + +/* +** COMMAND: test-database-names +** +** Print the names of the various database files: +** (1) The main repository database +** (2) The local check-out database +** (3) The global configuration database +*/ +void test_database_name_cmd(void){ + db_find_and_open_repository(OPEN_ANY_SCHEMA, 0); + fossil_print("Repository database: %s\n", g.zRepositoryName); + fossil_print("Local database: %s\n", g.zLocalDbName); + fossil_print("Config database: %s\n", g.zConfigDbName); +} + +/* +** Compute a "fingerprint" on the repository. A fingerprint is used +** to verify that the repository has not been replaced by a clone +** of the same repository. More precisely, a fingerprint is used to +** verify that the mapping between SHA3 hashes and RID values is unchanged. +** +** The check-out database ("localdb") stores RID values. When associating +** a check-out database against a repository database, it is useful to verify +** the fingerprint so that we know that the RID values in the check-out +** database still correspond to the correct entries in the BLOB table of +** the repository. +** +** The fingerprint is based on the RCVFROM table. When constructing a +** new fingerprint, use the most recent RCVFROM entry. (Set rcvid==0 to +** accomplish this.) When verifying an old fingerprint, use the same +** RCVFROM entry that generated the fingerprint in the first place. +** +** The fingerprint consists of the rcvid, a "/", and the MD5 checksum of +** the remaining fields of the RCVFROM table entry. MD5 is used for this +** because it is 4x faster than SHA3 and 5x faster than SHA1, and there +** are no security concerns - this is just a checksum, not a security +** token. +*/ +char *db_fingerprint(int rcvid, int iVersion){ + char *z = 0; + Blob sql = BLOB_INITIALIZER; + Stmt q; + if( iVersion==0 ){ + /* The original fingerprint algorithm used "quote(mtime)". But this + ** could give slightly different answers depending on how the floating- + ** point hardware is configured. For example, it gave different + ** answers on native Linux versus running under valgrind. */ + blob_append_sql(&sql, + "SELECT rcvid, quote(uid), quote(mtime), quote(nonce), quote(ipaddr)" + " FROM rcvfrom" + ); + }else{ + /* These days, we use "datetime(mtime)" for more consistent answers */ + blob_append_sql(&sql, + "SELECT rcvid, quote(uid), datetime(mtime), quote(nonce), quote(ipaddr)" + " FROM rcvfrom" + ); + } + if( rcvid<=0 ){ + blob_append_sql(&sql, " ORDER BY rcvid DESC LIMIT 1"); + }else{ + blob_append_sql(&sql, " WHERE rcvid=%d", rcvid); + } + db_prepare_blob(&q, &sql); + blob_reset(&sql); + if( db_step(&q)==SQLITE_ROW ){ + int i; + md5sum_init(); + for(i=1; i<=4; i++){ + md5sum_step_text(db_column_text(&q,i),-1); + } + z = mprintf("%d/%s",db_column_int(&q,0),md5sum_finish(0)); + } + db_finalize(&q); + return z; +} + +/* +** COMMAND: test-fingerprint +** +** Usage: %fossil test-fingerprint ?RCVID? +** +** Display the repository fingerprint using the supplied RCVID or +** using the latest RCVID if none is given on the command line. +** Show both the legacy and the newer version of the fingerprint, +** and the currently stored fingerprint if there is one. +*/ +void test_fingerprint(void){ + int rcvid = 0; + db_find_and_open_repository(OPEN_ANY_SCHEMA,0); + if( g.argc==3 ){ + rcvid = atoi(g.argv[2]); + }else if( g.argc!=2 ){ + fossil_fatal("wrong number of arguments"); + } + fossil_print("legacy: %z\n", db_fingerprint(rcvid, 0)); + fossil_print("version-1: %z\n", db_fingerprint(rcvid, 1)); + if( g.localOpen ){ + fossil_print("localdb: %z\n", db_lget("fingerprint","(none)")); + fossil_print("db_fingerprint_ok(): %d\n", db_fingerprint_ok()); + } + fossil_print("Fossil version: %s - %.10s %.19s\n", + RELEASE_VERSION, MANIFEST_DATE, MANIFEST_UUID); +} + +/* +** Set the value of the "checkout" entry in the VVAR table. +** If bWriteManifest is non-zero then also attempt to write the manifest +** files to disk. +** +** Also set "fingerprint" and "checkout-hash". +*/ +void db_set_checkout(int rid, int bWriteManifest){ + char *z; + if( bWriteManifest ) manifest_to_disk(rid); + db_lset_int("checkout", rid); + if (rid != 0) { + z = db_text(0,"SELECT uuid FROM blob WHERE rid=%d",rid); + db_lset("checkout-hash", z); + fossil_free(z); + z = db_fingerprint(0, 1); + db_lset("fingerprint", z); + fossil_free(z); + } +} + +/* +** Verify that the fingerprint recorded in the "fingerprint" entry +** of the VVAR table matches the fingerprint on the currently +** connected repository. Return true if the fingerprint is ok, and +** return false if the fingerprint does not match. +*/ +int db_fingerprint_ok(void){ + char *zCkout; /* The fingerprint recorded in the check-out database */ + char *zRepo; /* The fingerprint of the repository */ + int rc; /* Result */ + + if( !db_lget_int("checkout", 0) ){ + /* We have an empty check-out, fingerprint is still NULL. */ + return 2; + } + zCkout = db_text(0,"SELECT value FROM localdb.vvar WHERE name='fingerprint'"); + if( zCkout==0 ){ + /* This is an older check-out that does not record a fingerprint. + ** We have to assume everything is ok */ + return 2; + } + zRepo = db_fingerprint(atoi(zCkout), 1); + rc = fossil_strcmp(zCkout,zRepo)==0; + fossil_free(zRepo); + /* If the initial test fails, try again using the older fingerprint + ** algorithm */ + if( !rc ){ + zRepo = db_fingerprint(atoi(zCkout), 0); + rc = fossil_strcmp(zCkout,zRepo)==0; + fossil_free(zRepo); + } + fossil_free(zCkout); + return rc; +} + +/* +** Adds the given rid to the UNSENT table. +*/ +void db_add_unsent(int rid){ + db_multi_exec("INSERT OR IGNORE INTO unsent VALUES(%d)", rid); +} diff --git a/src/default.css b/src/default.css new file mode 100644 index 0000000..6218a0c --- /dev/null +++ b/src/default.css @@ -0,0 +1,2194 @@ +/* This CSS file holds the default implementations for all of fossil's + CSS classes. When /style.css is requested, the rules in this file + are emitted first, followed by (1) page-specific CSS (if any) and + (2) skin-specific CSS. +*/ +body { + z-index: 0 /* Used by robot.c:robot_proofofwork() and href.js */; +} +div.sidebox { + float: right; + background-color: white; + border-width: medium; + border-style: double; + margin: 10px; +} +div.sideboxTitle { + display: inline; + font-weight: bold; +} +div.sideboxDescribed { + display: inline; + font-weight: bold; +} +span.disabled { + color: red; +} +table.timelineTable { + border-spacing: 0px 2px; +} +.timelineDate { + white-space: nowrap; +} +span.timelineDisabled { + font-style: italic; + font-size: small; +} +tr.timelineCurrent { + padding: .1em .2em; + border: 1px dashed #446979; + box-shadow: 1px 1px 4px 1px rgba(0, 0, 0, 0.5); +} +.timelineSelected { + padding: .1em .2em; + border: 2px solid lightgray; + background-color: #ffc; + box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.5); +} +.timelineSecondary { + background-color: #cff; +} +tr.timelineSelected td { + border-radius: 0; + border-width: 0; +} +tr.timelineCurrent td { + border-radius: 0; + border-width: 0; +} +span.timelineLeaf { + font-weight: bold; +} +span.timelineHash { + font-weight: bold; +} +span.timelineHistDsp { + font-weight: bold; +} +td.timelineTime { + vertical-align: top; + text-align: right; + white-space: nowrap; +} +td.timelineGraph { + width: 20px; + text-align: left; + vertical-align: top; +} +span.timelineCompactComment { + cursor: pointer; +} +span.timelineEllipsis { + cursor: pointer; +} +.timelineModernCell, .timelineColumnarCell, .timelineDetailCell { + vertical-align: top; + text-align: left; + padding: 0.75em; + border-radius: 1em; +} +.timelineModernCell[id], .timelineColumnarCell[id], .timelineDetailCell[id] { + background-color: #efefef; +} +.timelineModernDetail { + font-size: 80%; + text-align: right; + float: right; + opacity: 0.75; + margin-top: 0.5em; + margin-left: 1em; +} +.tl-canvas { + margin: 0 6px 0 10px; +} +.tl-rail { + width: 18px; +} +.tl-mergeoffset { + width: 2px; +} +.tl-nodemark { + margin-top: 5px; +} +.tl-node { + width: 10px; + height: 10px; + border: 1px solid #000; + background: #fff; + cursor: pointer; +} +.tl-node.leaf:after { + content: ''; + position: absolute; + top: 3px; + left: 3px; + width: 4px; + height: 4px; + background: #000; +} +.tl-node.closed-leaf svg { + position: absolute; + top: 0px; + left: 0px; + width: 10px; + height: 10px; + color: #000; +} +.tl-node.sel:after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 6px; + height: 6px; + background: red; +} +.tl-arrow { + width: 0; + height: 0; + transform: scale(.999); + border: 0 solid transparent; +} +.tl-arrow.u { + margin-top: -1px; + border-width: 0 3px; + border-bottom: 7px solid #000; +} +.tl-arrow.u.sm { + border-bottom: 5px solid #000; +} +.tl-line { + background: #000; + width: 2px; +} +.tl-arrow.merge { + height: 1px; + border-width: 2px 0; +} +.tl-arrow.merge.l { + border-right: 3px solid #000; +} +.tl-arrow.merge.r { + border-left: 3px solid #000; +} +.tl-line.merge { + width: 1px; +} +.tl-arrow.cherrypick { + height: 1px; + border-width: 2px 0; +} +.tl-arrow.cherrypick.l { + border-right: 3px solid #000; +} +.tl-arrow.cherrypick.r { + border-left: 3px solid #000; +} +.tl-line.cherrypick.h { + width: 0px; + border-top: 1px dashed #000; + border-left: 0px dashed #000; + background: rgba(255,255,255,0); +} +.tl-line.cherrypick.v { + width: 0px; + border-top: 0px dashed #000; + border-left: 1px dashed #000; + background: rgba(255,255,255,0); +} +.tl-arrow.warp { + margin-left: 1px; + border-width: 3px 0; + border-left: 7px solid #600000; +} +.tl-line.warp { + background: #600000; +} +.tl-line.dotted.v { + width: 0px; + border-left-width: 2px; + border-left-style: dotted; + background: rgba(255,255,255,0); +} +.tl-tooltip { + text-align: center; + padding: 5px 1em; + border: 1px solid black; + border-radius: 6px; + position: absolute; + z-index: 100; + box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.75); +} + +span.tagDsp { + font-weight: bold; +} +span.wikiError { + font-weight: bold; + color: red; +} +span.infoTagCancelled { + font-weight: bold; + text-decoration: line-through; +} +span.infoTag { + font-weight: bold; +} +span.wikiTagCancelled { + text-decoration: line-through; +} +div.columns { + padding: 0 2em 0 2em; + max-width: 1000px; +} +div.columns > ul { + margin: 0; + padding: 0 0 0 1em; +} +div.columns > ul li:first-child { + margin-top:0px; +} +.columns li { + break-inside: avoid; + page-break-inside: avoid; +} +body.help .columns li { + white-space: nowrap /* keep command name aliases from wrapping */; +} +.filetree { + margin: 1em 0; + line-height: 1.5; +} +.filetree > ul { + display: inline-block; +} +.filetree ul { + margin: 0; + padding: 0; + list-style: none; +} +.filetree ul.collapsed { + display: none; +} +.filetree ul ul { + position: relative; + margin: 0 0 0 21px; +} +.filetree li { + position: relative; + margin: 0; + padding: 0; +} +.filetree li li:before { + content: ''; + position: absolute; + top: -.8em; + left: -14px; + width: 14px; + height: 1.5em; + border-left: 2px solid #aaa; + border-bottom: 2px solid #aaa; +} +.filetree li > ul:before { + content: ''; + position: absolute; + top: -1.5em; + bottom: 0; + left: -35px; + border-left: 2px solid #aaa; +} +.filetree li.last > ul:before { + display: none; +} +.filetree a { + position: relative; + z-index: 1; + display: table-cell; + min-height: 16px; + padding-left: 21px; + background-image: url("data:image/gif;base64,R0lGODlhEAAQAJEAAP\/\/\/y\ +EhIf\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAIvlIKpxqcfmgOUvoaqDSCxrEEfF14GqFX\ +ImJZsu73wepJzVMNxrtNTj3NATMKhpwAAOw=="); + background-position: center left; + background-repeat: no-repeat; +} +ul.browser { + list-style-type: none; + padding: 10px; + margin: 0px; + white-space: nowrap; +} +ul.browser li.file { + padding-top: 2px; +} +ul.browser li.file > a { + padding-left: 20px; + background-repeat: no-repeat; + background-position: 0px center; + background-image: url("data:image/gif;base64,R0lGODlhEAAQAJEAAP\/\/\/\ +yEhIf\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAIvlIKpxqcfmgOUvoaqDSCxrEEfF14Gq\ +FXImJZsu73wepJzVMNxrtNTj3NATMKhpwAAOw=="); +} +ul.browser li.dir { + padding-top: 2px; +} +ul.browser li.dir > a { + padding-left: 20px; + background-image: url("data:image/gif;base64,R0lGODlhEAAQAJEAAP/WVCIiI\ +v\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAInlI9pwa3XYniCgQtkrAFfLXkiFo1jaXpo+\ +jUs6b5Z/K4siDu5RPUFADs="); + background-repeat: no-repeat; + background-position: 0px center; +} +div.filetreeline { + display: table; + width: 100%; + white-space: nowrap; +} +.filetree .dir > div.filetreeline > a { + background-image: url("data:image/gif;base64,R0lGODlhEAAQAJEAAP/WVCIiI\ +v\/\/\/wAAACH5BAEHAAIALAAAAAAQABAAAAInlI9pwa3XYniCgQtkrAFfLXkiFo1jaXpo\ ++jUs6b5Z/K4siDu5RPUFADs="); +} +div.filetreeage { + display: table-cell; + padding-left: 1.5em; + text-align: right; + width: 8em; +} +div.filetreesize { + display: table-cell; + padding-left: 1em; + text-align: right; + width: 7em; +} +div.filetreeline:hover { + background-color: #eee; +} +table.login_out { + text-align: left; + margin-right: 10px; + margin-left: 10px; + margin-top: 10px; +} +div.captcha { + text-align: center; + padding: 1ex; +} +table.captcha { + margin: auto; + padding: 10px; + border-width: 4px; + border-style: double; + border-color: black; +} +pre.captcha { + font-size: 50%; +} +td.login_out_label { + text-align: center; +} +span.loginError { + color: red; +} +span.note { + font-weight: bold; +} +span.textareaLabel { + font-weight: bold; +} +table.usetupLayoutTable { + outline-style: none; + padding: 0; + margin: 25px; +} +td.usetupColumnLayout { + vertical-align: top +} +table.usetupUserList { + outline-style: double; + outline-width: 1px; + padding: 10px; +} +th.usetupListUser { + text-align: right; + padding-right: 20px; +} +th.usetupListCap { + text-align: center; + padding-right: 15px; +} +th.usetupListCon { + text-align: left; +} +td.usetupListUser { + text-align: right; + padding-right: 20px; + white-space:nowrap; +} +td.usetupListCap { + text-align: center; + padding-right: 15px; +} +td.usetupListCon { + text-align: left +} +div.ueditCapBox { + margin-right: 20px; + margin-bottom: 20px; +} +td.usetupEditLabel { + text-align: right; + vertical-align: top; + white-space: nowrap; +} +span.ueditInheritNobody { + color: green; + padding: .2em; +} +span.ueditInheritDeveloper { + color: red; + padding: .2em; +} +span.ueditInheritReader { + color: black; + padding: .2em; +} +span.ueditInheritAnonymous { + color: blue; + padding: .2em; +} +span.capability { + font-weight: bold; +} +span.usertype { + font-weight: bold; +} +span.usertype:before { + content:"'"; +} +span.usertype:after { + content:"'"; +} +p.missingPriv { + color: blue; +} +span.wikiruleHead { + font-weight: bold; +} +td.tktDspLabel { + text-align: right; +} +td.tktDspValue { + text-align: left; + vertical-align: top; + background-color: #d0d0d0; +} +td.tktTlOpen { + color: #800; +} +td.tktTlClosed { + color: #888; +} +span.tktError { + color: red; + font-weight: bold; +} +table.rpteditex { + float: right; + margin: 0; + padding: 0; + width: 125px; + text-align: center; + border-collapse: collapse; + border-spacing: 0; +} +table.report { + border: 1px solid #999; + margin: 1em 0 1em 0; + cursor: pointer; +} +td.rpteditex { + border-width: thin; + border-color: #000000; + border-style: solid; +} +div.endContent { + clear: both; +} +p.generalError { + color: red; +} +p.tktsetupError { + color: red; + font-weight: bold; +} +p.xfersetupError { + color: red; + font-weight: bold; +} +p.thmainError { + color: red; + font-weight: bold; +} +span.thTrace { + color: red; +} +p.reportError { + color: red; + font-weight: bold; +} +blockquote.reportError { + color: red; + font-weight: bold; +} +p.noMoreShun { + color: blue; +} +p.shunned { + color: blue; +} +span.brokenlink { + color: red; +} +ul.filelist { + margin-top: 3px; + line-height: 100%; +} +ul.filelist li { + padding-top: 1px; +} + +/* Rules governing diff layout and colors */ +table.diff { + width: 100%; + border-spacing: 0; + border-radius: 5px; + border: 1px solid black; + overflow: hidden; /* Prevent background from overlapping rounded borders. */ + font-size: 80%; +} +table.diff td.diffln{ + padding: 0; +} +table.diff td.diffln > pre{ + padding: 0 0.25em 0 0.5em; + margin: 0; +} +table.diff td { + vertical-align: top; + padding: 0; + overflow: hidden /*work around inner PRE slight overflow/overlap*/; +} +table.diff pre { + margin: 0 0 0 0; + padding: 0 0.5em; + line-height: 1.275/*for mobile: forum post e6f4ee7de98b55c0*/; + text-size-adjust: none + /* ^^^ attempt to keep mobile from inflating some text */; +} +table.diff pre > ins, +table.diff pre > del { + /* Fill platform-dependent color gaps caused by + inflated line-height */ + padding: 0.062em 0 0.062em 0; +} +table.diff pre > ins > *, +table.diff pre > del > *{ + /* Avoid odd-looking color swatches in conjunction with + (table.diff pre > ins/del) padding */ + padding: inherit; +} +table.diff td.diffln > pre { + padding: 0 0.35em 0 0.5em; +} +table.diff td > pre { + box-sizing: border-box; + /* Workaround for "slight wiggle" when using mouse-wheel in some FF + versions, apparently caused by the increased line-height forcing + these elements to be a *tick* larger than they should be but not + large enough to force a scroll bar to show up. */ + overflow-y: hidden; +} +tr.diffskip.jchunk { + /* jchunk gets added from JS to diffskip rows when they are + plugged into the /jchunk route. */ + background-color: aliceblue; + padding: 0; +} +tr.diffskip.jchunk > td { + padding: 0.25em 0.5em; + margin: 0; +} +tr.diffskip.jchunk:hover { + /*background-color: rgba(127,127,127,0.5); + cursor: pointer;*/ +} +tr.diffskip > td.chunkctrl { + text-align: left; +} +tr.diffskip > td.chunkctrl > div { + display: flex; + align-items: center; +} +tr.diffskip > td.chunkctrl > div > span.error { + padding: 0.25em 0.5em; + border-radius: 0.5em; +} +tr.diffskip > td.chunkctrl .jcbutton +/* class name .button breaks w/ some skins! */ { + min-width: 3.5ex; + max-width: 3.5ex; + text-align: center; + display: inline-block; + padding: 0.1em 1em; + margin: 0 1em 0 0; + background-color: rgba(127,127,127,0.2); + border-style: outset; + border-width: 0; + border-radius: 0.5em; + opacity: 0.7; +} +tr.diffskip > td.chunkctrl .jcbutton.up:not(.down){ + /* Simulate an arrow pointing up */ + border-radius: 3em 3em 0.25em 0.25em; +} +tr.diffskip > td.chunkctrl .jcbutton.down:not(.up){ + /* Simulate an arrow pointing down */ + border-radius: 0.25em 0.25em 3em 3em; +} +tr.diffskip > td.chunkctrl .jcbutton > span { + /* In order to increase the glyph size w/o increasing the em-based + button size or border-radius, we need an extra layer of DOM + element for the glyph. */ + font-size: 150%; +} +tr.diffskip > td.chunkctrl .jcbutton.up > span::before { + content: '⇡'; +} +tr.diffskip > td.chunkctrl .jcbutton.down > span::before { + content: '⇣'; +} +tr.diffskip > td.chunkctrl .jcbutton.up.down > span::before { + content: '⇡⇣'; +} +tr.diffskip > td.chunkctrl .jcbutton:hover { + cursor: pointer; + opacity: 1; + filter: contrast(1); +} +tr.diffchunk { + display: grid; + gap: 0px 0px; + grid-template-rows: 1fr; +} +table.splitdiff tr.diffchunk { + grid-template-columns: auto 1fr auto auto 1fr; + grid-template-areas: "difflnl difftxtl diffsep difflnr difftxtr"; +} +table.udiff tr.diffchunk { + grid-template-columns: auto auto auto 1fr; + grid-template-areas: "difflnl difflnr diffsep difftxtu"; +} +td.difflnl { grid-area: difflnl; } +td.difflnr { grid-area: difflnr; } +td.difftxtu { grid-area: difftxtu; } +td.difftxtl { grid-area: difftxtl; } +td.difftxtr { grid-area: difftxtr; } +td.diffln { + width: fit-content; + text-align: right; + padding: 0 1em 0 0; +} +td.difflne { + padding-bottom: 0.4em; +} +td.diffsep { + width: fit-content; + padding: 0 0.3em 0 0.5em; + grid-area: diffsep; +} +td.difftxt pre { + overflow-x: auto; +} +td.diffln ins { + background-color: #a0e4b2; + text-decoration: none; +} +td.diffln del { + background-color: #ffc0c0; + text-decoration: none; +} +td.difftxt del { + background-color: #ffe8e8; + text-decoration: none; +} +td.difftxt del > del { + background-color: #ffc0c0; + text-decoration: none; + font-weight: bold; +} +td.difftxt del > del.edit { + background-color: #c0c0ff; + text-decoration: none; + font-weight: bold; +} +td.difftxt ins { + background-color: #dafbe1; + text-decoration: none; +} +td.difftxt ins > ins { + background-color: #a0e4b2; + text-decoration: none; + font-weight: bold; +} +td.difftxt ins > ins.edit { + background-color: #c0c0ff; + text-decoration: none; + font-weight: bold; +} +body.tkt div.content li > table.udiff { + margin-left: 1.5em; + margin-top: 0.5em; +} +body.tkt div.content ol.tkt-changes > li:target > p > span { + border-bottom: 3px solid gold; +} +body.tkt div.content ol.tkt-changes > li:target > ol { + border-left: 1px solid gold; +} +body.tkt .tktCommentArea { + display: flex; + flex-direction: column; +} +body.tkt .newest-first-controls { + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} +body.tkt .tktCommentArea.reverse { + flex-direction: column-reverse; +} +body.cpage-ckout .file-change-line, +body.cpage-info .file-change-line, +body.cpage-vinfo .file-change-line, +body.cpage-ci .file-change-line, +body.cpage-vdiff .file-change-line { + margin-top: 16px; + margin-bottom: 16px; + margin-right: 1em /* keep it from nudging right up against the scrollbar-reveal zone */; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +span.modpending { + color: #b03800; + font-style: italic; +} +pre.th1result { + white-space: pre-wrap; + word-wrap: break-word; +} +pre.th1error { + white-space: pre-wrap; + word-wrap: break-word; + color: red; +} +pre.textPlain { + white-space: pre-wrap; + word-wrap: break-word; +} +.statistics-report-graph-line { + border: 2px solid #446979; + background-color: #446979; +} +.statistics-report-graph-extra { + border: 2px dashed #446979; + border-left-style: none; +} +.statistics-report-table-events th { + padding: 0 1em 0 1em; +} +.statistics-report-table-events td { + padding: 0.1em 1em 0.1em 1em; +} +.statistics-report-row-year { + text-align: left; +} +.statistics-report-week-number-label { + text-align: right; + font-size: 0.8em; +} +.statistics-report-week-of-year-list { + font-size: 0.8em; +} +#usetupEditCapability { + font-weight: bold; +} +table.adminLogTable { + text-align: left; +} +.adminLogTable .adminTime { + text-align: left; + vertical-align: top; + white-space: nowrap; +} +.fileage table { + border-spacing: 0; +} +.fileage tr:hover { + background-color: #eee; +} +.fileage td { + vertical-align: top; + text-align: left; + border-top: 1px solid #ddd; + padding-top: 3px; +} +.fileage td:first-child { + white-space: nowrap; +} +.fileage td:nth-child(2) { + padding-left: 1em; + padding-right: 1em; +} +.fileage td:nth-child(3) { + word-wrap: break-word; + max-width: 50%; +} +.brlist table { + border-spacing: 0; +} +.brlist table th { + text-align: left; + padding: 0px 1em 0.5ex 0px; + vertical-align: bottom; +} +.brlist table td { + padding: 0px 2em 0px 0px; + white-space: nowrap; +} +th.sort:after { + margin-left: .4em; + cursor: pointer; + text-shadow: 0 0 0 #000; +} +th.sort.none:after { + content: '\2666'; +} +th.sort.asc:after { + content: '\2193'; +} +th.sort.desc:after { + content: '\2191'; +} +span.snippet>mark { + background-color: inherit; + font-weight: bold; +} +div.searchForm { + text-align: center; +} +p.searchEmpty { + font-style: italic; +} +.clutter { + display: none; +} +table.label-value th { + vertical-align: top; + text-align: right; + padding: 0.2ex 1ex; +} +table.forum_post { + margin-top: 1ex; + margin-bottom: 1ex; + margin-left: 0; + margin-right: 0; + border-spacing: 0; +} +span.forum_author { + color: #888; + font-size: 75%; +} +span.forum_author::after { + content: " | "; +} +span.forum_age { + color: #888; + font-size: 85%; +} +span.forum_buttons { + font-size: 85%; +} +span.forum_buttons::before { + color: #888; + content: " | "; +} +span.forum_npost { + color: #888; + font-size: 75%; +} +table.forumeditform td { + vertical-align: top; + border-collapse: collapse; + padding: 1px; +} +div.forum_body p { + margin-top: 0; +} +div.forum-editor-widget{ + display: flex; + flex-direction: column; +} +div.forum-editor-widget > textarea { + max-width: initial; +} +td.form_label { + vertical-align: top; + text-align: right; +} +.debug { + background-color: #ffc; + border: 2px solid #ff0; +} +div.forumEdit { + border: 1px solid black; + padding-left: 1ex; + padding-right: 1ex; +} +div.forumTimeline { + border: 1px solid black; + padding-left: 1ex; + padding-right: 1ex; + max-width: 50em; + overflow: auto; +} +div.forumTimeline code { + white-space: pre-wrap; +} +div.markdown code { + white-space: pre-wrap; +} +div.forumTime { + border: 1px solid black; + padding-left: 1ex; + padding-right: 1ex; + margin-top: 1ex; + display: flex; + flex-direction: column; +} +div.forumClosed { +} +div.forumClosed > .forumPostBody { + opacity: 0.7; +} +div.forumClosed > .forumPostHdr::before { + content: "[CLOSED] "; +} +/*div.forumClosed > div.forumPostBody { + filter: blur(5px); +}*/ +div.forumpost-closure-warning { + margin-top: 1em; + margin-bottom: 1em; + border-style: solid; + padding: 0.25em 0.5em; + background: #f4f400bb; + /*font-weight: bold;*/ +} +div.forumpost-closure-warning input[type=submit] { + padding: 0.25em; +} +div.forumpost-single-controls { + /* UI controls along the bottom of a single post + ** in the thread view. */ +} +.forum div > form { + margin: 0.5em 0; + display: inline-block; +} +body.cpage-forumedit div > form, +body.cpage-forume2 div > form{ + width: 100%; +} +.forum div > form > * { + margin-bottom: 0.35em; +} +.forum-post-collapser { + /* Common style for the bottom-of-post and right-of-post + expand/collapse widgets. */ + font-size: 0.8em; + padding: 0; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0 0 0.5em 0.5em; + background-color: rgba(0, 0, 0, 0.05); + opacity: 0.8; + cursor: pointer; +} +.forum-post-collapser.bottom { + margin: 0 0 0.4em 0; + height: 1.75em; + line-height: 1.75em; + /* ^^^ Those sizes are finely tuned for the current selection of + arrow characters. If those change, these should, too. Remember that + FF/Chrome simply do not agree on alignment with most values :/. */ + display: flex; + flex-direction: row; + justify-content: space-between; +} +.forum-post-collapser.bottom > span { + margin: 0 1em 0 1em; + vertical-align: middle; +} +.forum-post-collapser.bottom > span::before { + content: "⇣⇣⇣"; +} +.forum-post-collapser.bottom.expanded > span::before { + content: "⇡⇡⇡" /*reminder: FF/Chrome cannot agree on alignment of ⮝*/; +} +div.forumPostBody{ + max-height: 50em; + overflow: auto; +} +div.forumPostBody.with-expander { + display: flex; + flex-direction: row; + overflow: auto; +} +div.forumPostBody.with-expander:not(.expanded) > :first-child { + overflow-y: hidden; +} +div.forumPostBody.with-expander > *:first-child { + /* Main content DIV/PRE */ + overflow: auto; + flex: 10 1 auto; +} +div.forumPostBody.with-expander.expanded > *:first-child { + margin-bottom: 0.5em /* try to suppress scroll bar */; +} +div.forumPostBody.with-expander .forum-post-collapser.right { + /* "Tap zone" for expansion of the post, sits to the right of the + post's content. */ + flex: 1 10 auto; + min-width: 1.25em; + max-width: 1.25em; + margin: 0 0 0 0.2em; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + border-radius: 0.1em; + cursor: pointer; + border-bottom: 0; + border-radius: 0 0.5em 0 0; +} +div.forumPostBody.with-expander .forum-post-collapser.right > span:before { + content: "⇣"; +} +div.forumPostBody.with-expander.expanded .forum-post-collapser.right > span:before { + content: "⇡"; +} +div.forumPostBody.expanded { + max-height: initial; +} +div.forumPostBody.shrunken { + /* When an expandable post is un-expanded, it is shrunkend down + to this size instead of its original size. */ + max-height: 8em; +} +span.forumPostReplyTitle { + /* thread title part of the page header when replying to a post */ + font-style: italic; +} + +div.forumSel { + background-color: #cef; +} +div.forumObs { + color: #bbb; +} + +div.setup_forum-column { + display: flex; + flex-direction: column; +} + +body.forum span.forum-status-selection { + white-space: nowrap; +} + +body.cpage-forum div.forumPosts tr[data-status] td.status { + /* Add a gap before the "X posts spanning Y time" labels, + ** which sometimes wrap and look odd without this gap. */ + padding-left: 1em; +} +body.cpage-forum div.forumPosts tr[data-status="open"] { +} +body.cpage-forum div.forumPosts tr[data-status="resolved"] { +} + +body.cpage-setup_forum > .content table { + margin-bottom: 1em; +} +body.cpage-setup_forum > .content table.bordered { + border: 1px solid; + border-radius: 0.25em; +} +body.cpage-setup_forum > .content table td, +body.cpage-setup_forum > .content table th { + text-align: left; +} +body.cpage-setup_forum table.forum-settings-list > tbody > tr > td { + min-width: 2em; +} + +#capabilitySummary { + text-align: center; +} +#capabilitySummary td { + padding-left: 3ex; + padding-right: 3ex; +} +#capabilitySummary th { + padding-left: 1ex; + padding-right: 1ex; +} +.capsumOff { + background-color: #bbb; +} +.capsumRead { + background-color: #bfb; +} +.capsumWrite { + background-color: #ffb; +} +label { + white-space: nowrap; +} +label[for] { + cursor: pointer; +} +button.copy-button, +button.copy-button:hover, +button.copy-button:focus, +button.copy-button:active { + width: 14px; + height: 14px; +/*Note: .24em is slightly smaller than the average width of a normal space.*/ + margin: -2px .24em 0 0; + padding: 0; + border: 0; + outline: 0; + background: none; + font-size: inherit; /* Required for horizontal spacing. */ + vertical-align: middle; + user-select: none; + cursor: pointer; +} +button.copy-button-flipped, +button.copy-button-flipped:hover, +button.copy-button-flipped:focus, +button.copy-button-flipped:active { + margin: -2px 0 0 .24em; +} +button.copy-button span { + display: block; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + border: 0; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' \ +viewBox='0,0,14,14'%3E%3Cpath style='fill:black;opacity:0' \ +d='M14,14H0V0h14v14z'/%3E%3Cpath style='fill:rgb(240,240,240)' \ +d='M1,0h6.6l2,2h1l3.4,3.4v8.6h-10v-2h-3z'/%3E%3Cpath style='fill:rgb(64,64,64)' \ +d='M2,1h5l3,3v7h-8z'/%3E%3Cpath style='fill:rgb(248,248,248)' \ +d='M3,2h3.6l2.4,2.4v5.6h-6z'/%3E%3Cpath style='fill:rgb(80,128,208)' \ +d='M4,5h4v1h-4zm0,2h4v1h-4z'/%3E%3Cpath style='fill:rgb(64,64,64)' \ +d='M5,3h5l3,3v7h-8z'/%3E%3Cpath style='fill:rgb(248,248,248)' \ +d='M10,4.4v1.6h1.6zm-4,-0.6h3v3h-3zm0,3h6v5.4h-6z'/%3E%3Cpath style='fill:rgb(80,128,208)' \ +d='M7,8h4v1h-4zm0,2h4v1h-4z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + cursor: pointer; +} +button.copy-button:enabled:active span { + background-size: 90%; +} +button.copy-button:disabled span { + filter: grayscale(1); + opacity: 0.4; +} +.nobr { + white-space: nowrap; +} +.accordion { + cursor: pointer; +} +.accordion_btn { + display: inline-block; + width: 16px; + height: 16px; + margin-right: .5em; + vertical-align: middle; +} +/* Note: the order of the next 3 entries should be + maintained for the hierarchical cascade to work. */ +.accordion > .accordion_btn_plus { + display: none; +} +.accordion_closed > .accordion_btn_minus { + display: none; +} +.accordion_closed > .accordion_btn_plus { + display: inline-block; +} +.accordion_panel { + overflow-y: clip; + transition: max-height 0.25s ease-out; +} +.error { + color: darkred; + background: yellow; +} +.warning { + color: black; + background: yellow; +} +.hidden, .initially-hidden { + /* The framework-wide way of hiding elements is to assign them th + .hidden class. To make them visible again, remove it. The + !important qualifiers are unfortunate but sometimes necessary + when hidden element has other classes which specify + visibility-related options. The .initially-hidden class is for + pages which need to show, e.g., a progress widget while a large + WASM blob loads. Elements aside from that load-time widget can be + made .initially-hidden and then have that class removed once the + long-running startup process is done. See /pikchrshow for an + example. */ + position: absolute !important; + opacity: 0 !important; + pointer-events: none !important; + display: none !important; +} +input { + max-width: 95%; +} +textarea { + max-width: 95%; +} +img { + max-width: 100%; +} +hr { + /* Needed to keep /dir README.txt from floating right in some skins */ + clear: both; +} + +/** + .tab-xxx: styles for fossil.tabs.js. +*/ +.tab-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: stretch; +} +.tab-container > #fossil-status-bar { + margin-top: 0; +} +.tab-container > .tabs { + padding: 0.25em; + margin: 0; + display: flex; + flex-direction: column; + border-width: 1px; + border-style: outset; + border-color: inherit; +} +.tab-container > .tabs > .tab-panel { + align-self: stretch; + flex: 10 1 auto; + display: block; + border: 0; + padding: 0; + margin: 0; +} +.tab-container > .tab-bar { + display: flex; + flex-direction: row; + flex: 1 10 auto; + align-self: stretch; + flex-wrap: wrap; +} +.tab-container > .tab-bar > .tab-button { + display: inline-block; + border-radius: 0.25em 0.25em 0 0; + margin: 0 0.1em; + padding: 0.25em 0.75em; + align-self: baseline; + border-color: inherit; + border-width: 1px; + border-bottom: none; + border-top-style: inset; + border-left-style: inset; + border-right-style: inset; + cursor: pointer; + opacity: 0.6; +} +.tab-container > .tab-bar > .tab-button.selected { + text-decoration: underline; + opacity: 1.0; + border-top-style: outset; + border-left-style: outset; + border-right-style: outset; +} + +/** + The flex-xxx classes can be used to create basic flexbox layouts + through the application of classes to the containing/contained + objects. +*/ +.flex-container { + display: flex; +} +.flex-container.flex-row { + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: center; +} +.flex-container .flex-grow { + flex-grow: 10; + flex-shrink: 0; +} +.flex-container .flex-shrink { + flex-grow: 0; + flex-shrink: 10; +} +.flex-container.flex-row.stretch { + flex-wrap: wrap; + align-items: baseline; + justify-content: stretch; + margin: 0; +} +.flex-container.flex-column { + flex-direction: column; + flex-wrap: wrap; + justify-content: center; + align-items: center; +} +.flex-container.flex-column.stretch { + align-items: stretch; + margin: 0; +} +.flex-container.child-gap-small > * { + margin: 0.25em; +} +#fossil-status-bar { + display: block; + border-width: 1px; + border-style: inset; + border-color: inherit; + min-height: 1.5em; + font-size: 1.2em; + padding: 0.2em; + margin: 0.25em 0; + flex: 0 0 auto; +} +.font-size-80 { + font-size: 80%; +} +.font-size-100 { + font-size: 100%; +} +.font-size-125 { + font-size: 125%; +} +.font-size-150 { + font-size: 150%; +} +.font-size-175 { + font-size: 175%; +} +.font-size-200 { + font-size: 200%; +} + +/** + .input-with-label is intended to be a wrapper element which contain + both a LABEL tag and an INPUT or SELECT control. The wrapper is + "necessary", as opposed to placing the INPUT in the LABEL, so that + we can include multiple INPUT elements (e.g. a set of radio + buttons). Note that these elements must sometimes be BLOCK elements + (e.g. DIV) so that certain nesting constructs are legal. +*/ +.input-with-label { + border: 1px inset rgba(128, 128, 128, 0.5); + border-radius: 0.25em; + padding: 0.1em; + margin: 0 0.5em; + display: inline-block + /* We would really like flex layout but changing that + currently introduces a good deal of UI breakage + to chase down. The advantage would be better alignment + of the contained elements. */; + cursor: default; + white-space: nowrap; +} +.submenu .input-with-label { + border: none; +} +.input-with-label > * { + vertical-align: middle; +} +.input-with-label > label { + display: inline; /* some skins set label display to block! */ + cursor: pointer; + white-space: nowrap; +} +.input-with-label > input { + margin: 0; +} +.input-with-label > button { + margin: 0; +} +.input-with-label > select { + margin: 0; +} +.input-with-label > input[type=text] { + margin: 0; +} +.input-with-label > textarea { + margin: 0; +} +/* Browsers are unfortunately inconsistent in how they + align checkboxes and radio buttons, even if they're + given the same vertical-align value. 'middle' seems to + be the least bad option, rather than the ideal. */ +.input-with-label > input[type=checkbox] { + vertical-align: middle; +} +.input-with-label > input[type=radio] { + vertical-align: middle; +} +.input-with-label > label { + font-weight: initial; + margin: 0 0.25em 0 0.25em; + vertical-align: middle; +} + +table.numbered-lines { + width: 100%; + table-layout: fixed /* required to keep ultra-wide code from exceeding + window width, and instead force a scrollbar + on them. */; +} +table.numbered-lines > tbody > tr { + line-height: 1.35; + white-space: pre; +} +table.numbered-lines > tbody > tr > td { + font-family: inherit; + font-size: inherit; + line-height: inherit; + white-space: inherit; + margin: 0; + vertical-align: top; + padding: 0.25em 0 0 0 /*prevents slight overlap at top */; +} +table.numbered-lines td.line-numbers { + width: 4.5em; +} +table.numbered-lines td.line-numbers > pre { + margin: 0.25em/*must match top PADDING of td.file-content + > pre > code*/ 0 0 0; + padding: 0; + line-height: inherit; + font-size: inherit; + font-family: inherit; +} +table.numbered-lines td.line-numbers span { + display: inline-block; + margin: 0; + padding: 0; + line-height: inherit; + font-size: inherit; + font-family: inherit; + cursor: pointer; + white-space: pre; + margin-right: 2px/*keep selection from nudging the right column */; + text-align: right; +} +table.numbered-lines td.line-numbers span:hover { + background-color: rgba(112, 112, 112, 0.25); +} +table.numbered-lines td.file-content { + padding-left: 0.25em; +} +table.numbered-lines td.file-content > pre, +table.numbered-lines td.file-content > pre > code { + margin: 0; + padding: 0; + line-height: inherit; + font-size: inherit; + font-family: inherit; + white-space: pre; + display: block/*necessary for certain skins!*/; +} +table.numbered-lines td.file-content > pre { +} +table.numbered-lines td.file-content > pre > code { + overflow: auto; + padding-left: 0.5em; + padding-right: 0.5em; + padding-top: 0.25em/*any top padding here must match the top MARGIN of + td.line-numbers's first span child or the + lines/code will get misaligned. */; + padding-bottom: 0.25em/*prevents a slight overlap at bottom from + triggering a scroller*/; +} +table.numbered-lines td.file-content > pre > code > * { + /* Defense against syntax highlighters indirectly messing up these + properties... */ + line-height: inherit; + font-size: inherit; + font-family: inherit; +} +table.numbered-lines td.line-numbers span.selected-line/*replacement*/ { + font-weight: bold; + color: blue; + background-color: #d5d5ff; + border: 1px blue solid; + border-top-width: 0; + border-bottom-width: 0; + padding: 0; + margin: 0; +} +table.numbered-lines td.line-numbers span.selected-line.start { + border-top-width: 1px; + margin-top: -1px/*restore alignment*/; +} +table.numbered-lines td.line-numbers span.selected-line.end { + border-bottom-width: 1px; + margin-top: -1px/*restore alignment*/; +} +table.numbered-lines td.line-numbers span.selected-line.start.end { + margin-top: -2px/*restore alignment*/; +} + +.fossil-tooltip { + text-align: center; + padding: 0.2em 1em; + border: 1px solid black; + border-radius: 0.5em; + position: absolute; + display: inline-block; + z-index: 19/*below default skin's hamburger popup*/; + box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75); + background-color: inherit; + color: inherit; +} +.fossil-PopupWidget { + /* This class is ALWAYS set on every fossil.PopupWidget instance, in + addition to client/app-configured classes. It should not get any + style - it is only used for DOM element selecting/filtering + purposes. */ +} +.fossil-toast-message { + /* "toast"-style popup message. + See fossil.popupwidget:toast() */ + position: absolute; + display: block; + z-index: 1001; + text-align: left; + padding: 0.15em 0.5em; + margin: 0; + font-size: 1em; + border-width: 1px; + border-style: solid; + border-color: rgba( 127, 127, 127, 0.75 ); + border-radius: 0.25em; + background-color: rgba(20, 20, 20, 1) + /* problem: if we inherit the color it may either be + transparent or inherit translucency via the + skin, leaving it unreadable. Since we set the bg + color we must also set the fg color. */; + color: rgba(235, 235, 235, 0.9); +} +.fossil-PopupWidget a, +.fossil-PopupWidget a:visited { + color: initial; +} +.fossil-toast-message.error, +.fossil-toast-message.warning { + background: yellow; +} +.fossil-toast-message.error { + font-weight: bold; + color: darkred; + border-color: darkred; +} +.fossil-toast-message.warning { + color: black; +} + +blockquote.file-content { + /* file content block in the /file page */ + margin: 0 1em; +} + +/* Generic sidebar styling inherited by skins that don't make their own + * arrangements. */ +.markdown blockquote, p.blockquote, .sidebar { + background-color: rgba(0, 0, 0, 0.05); + border-left: 3px solid #777; + padding: 0.1em 1em; +} +.sidebar { + /* Generic form that can be applied to any block element. */ + font-size: 90%; +} +div.sidebar { + /* Special exception for div-type sidebars, where there is no p + * wrapper inside to give us the extra padding we want. */ + padding: 1em; +} +div.sidebar:not(.no-label):before { + content: "Sidebar: "; + font-weight: bold; +} + + +/** + Circular "help" buttons intended to be placed to the right of + another element and hold text text for it. These typically get + initialized automatically at page startup via + fossil.popupwidget.js, and can be manually initialized/created + using window.fossil.helpButtonlets.setup/create(). All of their + child content (plain text and/or DOM elements) gets moved out of + the DOM and shown in a singleton popup when they are clicked. They + may be SPAN elements if their children are all inline elements, + otherwise they must be DIVs (block elements) so that nesting of + block-element content is legal. +*/ +.help-buttonlet { + display: inline-block; + min-width: 1em; + max-width: 1em; + min-height: 1em; + max-height: 1em; + cursor: pointer; + margin: 0 0 0 0.35em; + background-image: /* white question mark on blue circular background */ + url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' \ +viewBox='0 0 15.867574 15.867574'%3e%3ccircle cx='7.9337869' cy='7.9337869' r='7.9337869' \ +style='fill:%23f0f0f0;stroke-width:1' /%3e%3ccircle cx='7.9337869' cy='7.9337869' \ +r='6.9662519' style='fill:%23404040;stroke-width:1' /%3e%3ccircle cx='7.9337869' \ +cy='7.9337869' r='5.9987168' style='fill:%235080d0;stroke-width:1' /%3e%3cpath \ +d='M 9.2253789,9.8629486 H 6.5997716 v -0.356384 q 0,-0.5963983 0.2400139,-1.0546067 \ +0.240014,-0.4654816 1.0109681,-1.1782504 L 8.316235,6.8518647 Q 8.7308046,6.473661 \ +8.9199065,6.1390961 9.1162816,5.8045312 9.1162816,5.4699662 q 0,-0.5091205 -0.3491113,-0.7927734 \ +-0.3491111,-0.2909259 -0.9746021,-0.2909259 -0.5891252,0 -1.2728012,0.247287 \ +-0.6836761,0.240014 -1.4255375,0.720042 V 3.0698267 q 0.8800513,-0.3054724 1.6073661,-0.4509353 \ +0.7273151,-0.145463 1.403718,-0.145463 1.7746486,0 2.7056104,0.727315 0.930965,0.720042 \ +0.930965,2.1092135 0,0.7127686 -0.283654,1.2800746 -0.283652,0.5600324 -0.967329,1.2073428 \ +L 10.025425,8.2119439 Q 9.530851,8.6628792 9.3781148,8.9392588 9.2253789,9.2083654 \ +9.2253789,9.535657 Z M 6.5997716,10.939376 h 2.6256073 v 2.589241 H 6.5997716 Z' \ +style='fill:%23f8f8f8;stroke-width:1.35412836' /%3e%3c/svg%3e "); + background-repeat: no-repeat; + background-position: center; + /* When not using a background image, this additional style works + reasonably well along with a ::before content of "?": */ + /*border-width: 1px; + border-style: outset; + border-radius: 0.5em; + font-size: 100%; + font-family: monspace; + font-weight: 700; + overflow: hidden; + background-color: rgba(54, 54, 255,1); + color: rgb(255, 255, 255); + text-align: center; + line-height: 1; */ +} +/*.help-buttonlet::before { + content: "?"; +}*/ +/** + We really want to hide all help text via CSS but CSS cannot select + TEXT nodes. Thus we move them out of the way programmatically + during initialization. +*/ +.help-buttonlet > *{} + +/** + CSS class for PopupWidget which wraps .help-buttonlet content. + They also have class fossil-tooltip. We need an overly-exact + selector here to be certain that this class's style overrides + that of fossil-tooltip. +*/ +.fossil-tooltip.help-buttonlet-content { + cursor: default; + text-align: left; + border-style: outset; +} + +noscript > .error { + /* Part of the style_emit_noscript_for_js_page() interface. */ + padding: 1em; + font-size: 150%; +} + +/************************************************************ + pikchr... + DOM structure: + <DIV.pikchr-wrapper> + <DIV.pikchr-svg> + <SVG.pikchr>...</SVG> + </DIV.pikchr-svg> + <DIV.pikchr-src> + <PRE>pikchr source code</PRE> + <SPAN class='hidden'><A>link to open pikchr in /pikchrshow</A></SPAN> + <!-- ^^^ is unhidden and activated by JS code --> + </DIV.pikchr-src> + </DIV.pikchr-wrapper> + +************************************************************/ +div.pikchr-wrapper {/*outer wrapper elem for a pikchr construct*/} +div.pikchr-svg {/*wrapper for SVG.pikchr element*/} +svg.pikchr {/*pikchr SVG*/ + width: 100%/*necessary for SOME SVGs for Chrome!*/; +} + +div.pikchr-src { + /*Wrapper for source code view of a pikchr (see fossil.pikchr.js)*/ + display: flex; + flex-direction: column; +} +div.pikchr-src > pre { + /*Source code for a pikchr*/ + box-sizing: border-box; + text-align: left; +} +div.pikchr-src > span { + /*Wrapper for a link to open a pikchr in /pikchrshow*/ + margin-top: 0.5em; + margin-bottom: 0.5em; + font-size: 85%; +} +div.pikchr-src > span::before { + content: "["; +} +div.pikchr-src > span::after { + content: "]"; +} +/* The .source-inline class tells the .source class that the + source view, when enabled, should be "inline" (same position + as the graphic), else the sources are shifted to the left as + if they were "plain text". */ +div.pikchr-wrapper.center:not(.source), +div.pikchr-wrapper.center.source.source-inline{ + text-align: center; + /* Reminder for The Future: this impl also works: + + display: grid; place-items: center; + + and does not require setting display:inline-block on the relevant + child items, but caniuse.com/css-grid suggests that some + still-seemingly-legitimate browsers don't support grid mode. */ +} +div.pikchr-wrapper.center > div.pikchr-svg { + width: 100%/*necessary for Chrome!*/; +} +div.pikchr-wrapper.center:not(.source) > div.pikchr-src, +div.pikchr-wrapper.center:not(.source) > div.pikchr-svg, +/* ^^^ Centered non-source-view elements */ +div.pikchr-wrapper.center.source.source-inline div.pikchr-src, +div.pikchr-wrapper.center.source.source-inline > div.pikchr-svg +/* ^^^ Centered inline-source-view elements */{ + display:inline-block/*allows parent text-align to do the alignment*/; + /* ^^^^ Browser incompatibility: inline-block causes the centered + pikchr to shrink to the point of illegiblity in Chrome. The + closest match on Chrome seems to be using 'unset', which centers + by virtue of stretching it to the width of the window. Similarly, + using {display: grid; place-items: center} centers and sizes well + on FF but Chrome shrinks it in the same way. */ +} +div.pikchr-wrapper.indent:not(.source), +div.pikchr-wrapper.indent.source.source-inline{ + margin-left: 4em; +} +div.pikchr-wrapper.float-left:not(.source), +div.pikchr-wrapper.float-left.source.source-inline { + float: left; + padding: 1em 2em; +} +div.pikchr-wrapper.float-right:not(.source), +div.pikchr-wrapper.float-right.source.source-inline{ + float: right; + padding: 1em 2em; +} + +/* For pikchr-wrapper.source mode, toggle pre.pikchr-src and + svg.pikchr visibility... */ +div.pikchr-wrapper.source > div.pikchr-src { + /* Source code ^^^^^^^ is visible, else it is hidden */ +} +div.pikchr-wrapper:not(.source) > div.pikchr-src { + /* Hide sources when image is being shown. */ + position: absolute !important; + opacity: 0 !important; + pointer-events: none !important; + display: none !important; +} +div.pikchr-wrapper.source > div.pikchr-svg { + /* Hide image when sources are being shown. */ + position: absolute !important; + opacity: 0 !important; + pointer-events: none !important; + display: none !important; +} + + +/* An icon element intended for use as a button/menu for + accessing app-specific settings. */ +.settings-icon { + /* Icon source: https://de.wikipedia.org/wiki/Datei:OOjs_UI_icon_settings.svg + MIT License. */ + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg \ +xmlns='http://www.w3.org/2000/svg' width='24' height='24' \ +viewBox='0, 0, 24, 24'%3e%3cg id='settings'%3e%3cpath id='gear' \ +d='M3 4h3v2h-3zM12 4h9v2h-9zM8 3h2c.552 0 1 .448 1 1v2c0 .552-.448 1-1 1h-2c-.552 \ +0-1-.448-1-1v-2c0-.552.448-1 1-1zM3 11h9v2h-9zM18 11h3v2h-3zM14 10h2c.552 0 1 .448 \ +1 1v2c0 .552-.448 1-1 1h-2c-.552 0-1-.448-1-1v-2c0-.552.448-1 1-1zM3 18h6v2h-6zM15 \ +18h6v2h-6zM11 17h2c.552 0 1 .448 1 1v2c0 .552-.448 1-1 1h-2c-.552 \ +0-1-.448-1-1v-2c0-.552.448-1 1-1z'/%3e%3c/g%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: center; + display: inline-block; + min-height: 1em; + max-height: 1em; + min-width: 1em; + max-width: 1em; + margin: 0; + padding: 0.2em/*needed to avoid image truncation*/; + border: 1px solid rgba(0,0,0,0.0)/*avoid resize when hover style kicks in*/; + cursor: pointer; + border-radius: 0.25em; +} +.settings-icon:hover { + border: 1px outset rgba(127,127,127,1); +} +body.fossil-dark-style .settings-icon { + filter: invert(100%); +} + +body.branch .brlist > table > tbody > tr:hover:not(.selected), +body.branch .brlist > table > tbody > tr.selected { + background-color: #ffc; +} +body.branch .brlist > table > tbody td:first-child > input { + cursor: pointer; +} +body.branch .brlist > table > tbody > tr > td:nth-child(1) { + display: flex; + flex-direction: row; + justify-content: space-between; +} +body.branch .submenu > a.timeline-link { + display: none; +} +body.branch .submenu > a.timeline-link.selected { + display: inline; +} + +/* Candidate fonts for various forms of monospaced text. Collected here + * to avoid repeating this long list of fonts. */ +code, kbd, pre, samp, tt, var, + div.markdown ol.footnotes > li.fn-joined > sup.fn-joined, + table.numbered-lines > tbody > tr, + tr.diffskip > td.chunkctrl, + #fossil-status-bar, + .monospace { + font-family: "MesloLGSDZ Nerd Font Mono", /* 2025 hotness */ + "Source Code Pro", /* 2012 hotness */ + "Menlo", "Monaco", "SF Mono", /* Safari reverted to Courier in 2022 */ + monospace; /* let OS/browser default take over */ +} + +div.markdown > ol.footnotes { + font-size: 90%; +} +div.markdown > ol.footnotes > li { + margin-bottom: 0.5em; +} +div.markdown ol.footnotes > li.fn-joined > sup.fn-joined { + color: gray; +} +div.markdown ol.footnotes > li.fn-joined > sup.fn-joined::after { + content: "(joined from multiple locations) "; +} +div.markdown ol.footnotes > li.fn-misreference { + margin-top: 0.75em; + margin-bottom: 0.75em; +} +div.markdown ol.footnotes > li.fn-toodeep > i, +div.markdown ol.footnotes > li.fn-misreference, +div.markdown ol.footnotes > li.fn-unreferenced { + color: gray; +} +div.markdown ol.footnotes > li.fn-misreference > span { + color: red; +} +div.markdown ol.footnotes > li.fn-misreference > span::after { + content: " (use of undefined label)."; +} +div.markdown ol.footnotes > li.fn-unreferenced { + padding-left: 0.5em; +} +div.markdown ol.footnotes > li.fn-unreferenced > code { + color: red; +} +div.markdown ol.footnotes > li.fn-unreferenced > i::after { + content: " was defined but is not referenced"; +} +div.markdown ol.footnotes > li.fn-toodeep > i::after { + content: " depth of nesting of inline footnotes exceeded the limit"; +} +div.markdown ol.footnotes > li.fn-toodeep > pre, +div.markdown ol.footnotes > li.fn-unreferenced > pre { + color: gray; + font-size: 85%; + padding-left: 0.5em; + margin-top: 0.25em; + border-left: 2px solid red; +} +div.markdown ol.footnotes > li.fn-toodeep > pre { + margin-left: 0.5em; +} +div.markdown > ol.footnotes > li > .fn-backrefs { + margin-right: 0.5em; + font-weight: bold; +} +div.markdown > ol.footnotes > li > .fn-backrefs > a, +div.markdown sup.noteref > a { + padding-left: 2px; + padding-right: 2px; +} +div.markdown sup.noteref.misref, +div.markdown sup.noteref.misref > a { + color: red; + font-size: 90%; +} +div.markdown sup.noteref > a:target, +div.markdown span.notescope:target > sup.noteref > a, +div.markdown span.notescope:hover > sup.noteref > a, +div.markdown > ol.footnotes > li > .fn-backrefs > a:target { + background: gold; +} +div.markdown span.notescope:hover, +div.markdown span.notescope:target { + border-bottom: 2px solid gold; +} + +/* Cause <dd> elements to be aligned complete to the +** right of their <dt> on help pages. */ +dl.helpOptions { + display: grid; + grid-template-columns: max-content 1fr; + column-gap: 1rem; +} +dl.helpOptions > dt { + grid-column: 1; +} +dl.helpOptions > dd { + grid-column: 2; + margin: 0; +} +div.helpPage blockquote { + margin-left: 0.2em; +} + +/* .attach* = styles for file attachments */ +section.attachlist {} +details.attachlist > summary { + cursor: pointer; +} +/* .Attacher is the top container element used by the JS Attacher + class in fossil.attach.js. */ +.Attacher { + margin-bottom: 1em; + display: flex; + flex-direction: column; + gap: 0.75em; +} +.Attacher.reverse { + flex-direction: column-reverse; +} +.Attacher .attach-row { + display: flex; + flex-direction: column; + gap: 0.5em; + padding: 0.75em; + border: 1px dashed #ccc; + border-radius: 0.25em; + background-color: #fafafa; +} +.Attacher .error { + padding: 0.5em; + background-color: #d32f2f; + color: #fff; +} +.Attacher .error a { + color: inherit; + text-decoration: underline; +} +body.fossil-dark-style .Attacher .attach-row { + background-color: initial; +} +.Attacher .attach-dropzone { + padding: 1em; + text-align: center; + background: #ffffff; + border: 1px solid #ddd; + cursor: pointer; + border-radius: 0.25em; + transition: background-color 0.15s linear; + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} +body.fossil-dark-style .Attacher .attach-dropzone{ + background: initial; +} +.Attacher .attach-dropzone.populated { + background-color: #f1f8e9; + border-color: #8bc34a; + border-style: solid; + text-align: left; +} +body.fossil-dark-style .Attacher .attach-dropzone.populated{ + background-color: initial; +} +.Attacher .attach-dropzone.dragover { + background-color: #e1f5fe; + color: black; + border-color: #03a9f4; +} +body.fossil-dark-style .Attacher .attach-dropzone.dragover{ + background-color: #e1f5fe; + border-color: #03a9f4; +} +.Attacher .thumbnail { + max-width: 10em; + max-height: 10em; + margin: 0 1em; +} +.Attacher .attach-row-info{ + font-family: monospace; + flex-grow: 1; + display: flex; + flex-direction: column; +} +.Attacher .attach-filename {} +.Attacher .attach-size {/*size and mimetype*/} +.Attacher .attach-desc { + max-width: initial; + width: 100%; + box-sizing: border-box; + min-height: 4em; + padding: 0.5em; + font-family: inherit; + resize: vertical; +} +.Attacher .attach-row-remove { + align-self: center; + padding: 0.25em 0.75em; + margin-left: 1em; + background-color: #d32f2f; + color: #fff; + border: none; + border-radius: 0.25em; + cursor: pointer; + font-weight: bold; +} +.Attacher .attach-row-remove:hover { + background-color: #b71c1c; +} +.Attacher .attach-controls { + display: flex; + flex-direction: row; + gap: 1em; +} +.Attacher .attach-controls .attach-add-button { + padding: 0.5em 1em; + cursor: pointer; + flex-grow: 2; + line-height: initial/*work around an inherited alignment quirk*/; +} + +/* .animate-X and their associated @keyframes are used by various + widgets to animate their comings and goings. */ +.animate-entrance { + animation: slideFadeIn 0.25s linear forwards; + transform-origin: top; + overflow: hidden /*prevent content bleeding during expansion*/; +} +.animate-exit { + animation: slideFadeOut 0.25s linear forwards; + transform-origin: top; + overflow: hidden; +} +@keyframes slideFadeIn { + 0% { + opacity: 0; + transform: translateY(-1em/*must match slideFadeOut*/); + max-height: 0; + } + 100% { + opacity: 1; + transform: translateY(0); + max-height: 100em /*a value safely larger than the widget*/; + } +} +@keyframes slideFadeOut { + 0% { + opacity: 1; + transform: translateY(0); + max-height: 100em /*must match slideFadeIn*/; + } + 100% { + opacity: 0; + transform: translateY(-1em/*must match slideFadeIn*/); + max-height: 0; + padding-top: 0; + padding-bottom: 0; + margin-top: 0; + margin-bottom: 0; + } +} + +/* Objects in the "desktoponly" class are invisible on mobile */ +@media screen and (max-width: 600px) { + .desktoponly { + display: none; + } +} +/* Float sidebars to the right of the main content only if there's room. */ +@media screen and (min-width: 600px) { + .sidebar { + float: right; + max-width: 33%; + margin-left: 1em; + } +} +/* Objects in the "wideonly" class are invisible only on wide-screen desktops */ +@media screen and (max-width: 1200px) { + .wideonly { + display: none; + } +} diff --git a/src/delta.c b/src/delta.c new file mode 100644 index 0000000..a592e91 --- /dev/null +++ b/src/delta.c @@ -0,0 +1,729 @@ +/* +** Copyright (c) 2006 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) + +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This module implements the delta compress algorithm. +** +** Though developed specifically for fossil, the code in this file +** is generally applicable and is thus easily separated from the +** fossil source code base. Nothing in this file depends on anything +** else in fossil. +*/ +#include "config.h" +#include <stdio.h> +#include <assert.h> +#include <stdlib.h> +#include <string.h> +#include "delta.h" + +/* +** Macros for turning debugging printfs on and off +*/ +#if 0 +# define DEBUG1(X) X +#else +# define DEBUG1(X) +#endif +#if 0 +#define DEBUG2(X) X +/* +** For debugging: +** Print 16 characters of text from zBuf +*/ +static const char *print16(const char *z){ + int i; + static char zBuf[20]; + for(i=0; i<16; i++){ + if( z[i]>=0x20 && z[i]<=0x7e ){ + zBuf[i] = z[i]; + }else{ + zBuf[i] = '.'; + } + } + zBuf[i] = 0; + return zBuf; +} +#else +# define DEBUG2(X) +#endif + +#if INTERFACE +/* +** The "u32" type must be an unsigned 32-bit integer. Adjust this +*/ +typedef unsigned int u32; + +/* +** Must be a 16-bit value +*/ +typedef short int s16; +typedef unsigned short int u16; + +#endif /* INTERFACE */ + +/* +** The width of a hash window in bytes. The algorithm only works if this +** is a power of 2. +*/ +#define NHASH 16 + +/* +** The current state of the rolling hash. +** +** z[] holds the values that have been hashed. z[] is a circular buffer. +** z[i] is the first entry and z[(i+NHASH-1)%NHASH] is the last entry of +** the window. +** +** Hash.a is the sum of all elements of hash.z[]. Hash.b is a weighted +** sum. Hash.b is z[i]*NHASH + z[i+1]*(NHASH-1) + ... + z[i+NHASH-1]*1. +** (Each index for z[] should be module NHASH, of course. The %NHASH operator +** is omitted in the prior expression for brevity.) +*/ +typedef struct hash hash; +struct hash { + u16 a, b; /* Hash values */ + u16 i; /* Start of the hash window */ + char z[NHASH]; /* The values that have been hashed */ +}; + +/* +** Initialize the rolling hash using the first NHASH characters of z[] +*/ +static void hash_init(hash *pHash, const char *z){ + u16 a, b, i; + a = b = z[0]; + for(i=1; i<NHASH; i++){ + a += z[i]; + b += a; + } + memcpy(pHash->z, z, NHASH); + pHash->a = a & 0xffff; + pHash->b = b & 0xffff; + pHash->i = 0; +} + +/* +** Advance the rolling hash by a single character "c" +*/ +static void hash_next(hash *pHash, int c){ + u16 old = pHash->z[pHash->i]; + pHash->z[pHash->i] = c; + pHash->i = (pHash->i+1)&(NHASH-1); + pHash->a = pHash->a - old + c; + pHash->b = pHash->b - NHASH*old + pHash->a; +} + +/* +** Return a 32-bit hash value +*/ +static u32 hash_32bit(hash *pHash){ + return (pHash->a & 0xffff) | (((u32)(pHash->b & 0xffff))<<16); +} + +/* +** Compute a hash on NHASH bytes. +** +** This routine is intended to be equivalent to: +** hash h; +** hash_init(&h, zInput); +** return hash_32bit(&h); +*/ +static u32 hash_once(const char *z){ + u16 a, b, i; + a = b = z[0]; + for(i=1; i<NHASH; i++){ + a += z[i]; + b += a; + } + return a | (((u32)b)<<16); +} + +/* +** Write an base-64 integer into the given buffer. +*/ +static void putInt(unsigned int v, char **pz){ + static const char zDigits[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"; + /* 123456789 123456789 123456789 123456789 123456789 123456789 123 */ + int i, j; + char zBuf[20]; + if( v==0 ){ + *(*pz)++ = '0'; + return; + } + for(i=0; v>0; i++, v>>=6){ + zBuf[i] = zDigits[v&0x3f]; + } + for(j=i-1; j>=0; j--){ + *(*pz)++ = zBuf[j]; + } +} + +/* +** Read bytes from *pz and convert them into a positive integer. When +** finished, leave *pz pointing to the first character past the end of +** the integer. The *pLen parameter holds the length of the string +** in *pz and is decremented once for each character in the integer. +*/ +static unsigned int getInt(const char **pz, int *pLen){ + static const signed char zValue[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, + -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + unsigned int v = 0; + int c; + unsigned char *z = (unsigned char*)*pz; + unsigned char *zEnd = z + (*pLen); + while( z<zEnd && (c = zValue[*z])>=0 ){ + v = (v<<6) + c; + z++; + } + *pLen -= (int)(z - (unsigned char*)*pz); + *pz = (char*)z; + return v; +} + +/* +** Return the number digits in the base-64 representation of a positive integer +*/ +static int digit_count(int v){ + unsigned int i; + int x; + for(i=1, x=64; v>=x; i++, x <<= 6){} + return i; +} + +#ifdef __GNUC__ +# define GCC_VERSION (__GNUC__*1000000+__GNUC_MINOR__*1000+__GNUC_PATCHLEVEL__) +#else +# define GCC_VERSION 0 +#endif + +/* +** Compute a 32-bit big-endian checksum on the N-byte buffer. If the +** buffer is not a multiple of 4 bytes length, compute the sum that would +** have occurred if the buffer was padded with zeros to the next multiple +** of four bytes. +*/ +static unsigned int checksum(const char *zIn, size_t N){ + static const int byteOrderTest = 1; + const unsigned char *z = (const unsigned char *)zIn; + unsigned sum = 0; + if( N>0 ){ + const unsigned char *zEnd = (const unsigned char*)&zIn[N&~3]; + assert( (z - (const unsigned char*)0)%4==0 ); /* Four-byte alignment */ + if( 0==*(char*)&byteOrderTest ){ + /* This is a big-endian machine */ + while( z<zEnd ){ + sum += *(unsigned*)z; + z += 4; + } + }else{ + /* A little-endian machine */ + #if GCC_VERSION>=4003000 + while( z<zEnd ){ + sum += __builtin_bswap32(*(unsigned*)z); + z += 4; + } + #elif defined(_MSC_VER) && _MSC_VER>=1300 + while( z<zEnd ){ + sum += _byteswap_ulong(*(unsigned*)z); + z += 4; + } + #else + unsigned sum0 = 0; + unsigned sum1 = 0; + unsigned sum2 = 0; + while(N >= 16){ + sum0 += ((unsigned)z[0] + z[4] + z[8] + z[12]); + sum1 += ((unsigned)z[1] + z[5] + z[9] + z[13]); + sum2 += ((unsigned)z[2] + z[6] + z[10]+ z[14]); + sum += ((unsigned)z[3] + z[7] + z[11]+ z[15]); + z += 16; + N -= 16; + } + while(N >= 4){ + sum0 += z[0]; + sum1 += z[1]; + sum2 += z[2]; + sum += z[3]; + z += 4; + N -= 4; + } + sum += (sum2 << 8) + (sum1 << 16) + (sum0 << 24); + #endif + } + switch(N&3){ + case 3: sum += (z[2] << 8); + case 2: sum += (z[1] << 16); + case 1: sum += (z[0] << 24); + default: ; + } + } + return sum; +} + +/* +** Create a new delta. +** +** The delta is written into a preallocated buffer, zDelta, which +** should be at least 60 bytes longer than the target file, zOut. +** The delta string will be NUL-terminated, but it might also contain +** embedded NUL characters if either the zSrc or zOut files are +** binary. This function returns the length of the delta string +** in bytes, excluding the final NUL terminator character. +** +** Output Format: +** +** The delta begins with a base64 number followed by a newline. This +** number is the number of bytes in the TARGET file. Thus, given a +** delta file z, a program can compute the size of the output file +** simply by reading the first line and decoding the base-64 number +** found there. The delta_output_size() routine does exactly this. +** +** After the initial size number, the delta consists of a series of +** literal text segments and commands to copy from the SOURCE file. +** A copy command looks like this: +** +** NNN@MMM, +** +** where NNN is the number of bytes to be copied and MMM is the offset +** into the source file of the first byte (both base-64). If NNN is 0 +** it means copy the rest of the input file. Literal text is like this: +** +** NNN:TTTTT +** +** where NNN is the number of bytes of text (base-64) and TTTTT is the text. +** +** The last term is of the form +** +** NNN; +** +** In this case, NNN is a 32-bit big endian checksum of the output file +** that can be used to verify that the delta applied correctly. All +** numbers are in base-64. +** +** Pure text files generate a pure text delta. Binary files generate a +** delta that may contain some binary data. +** +** Algorithm: +** +** The encoder first builds a hash table to help it find matching +** patterns in the source file. 16-byte chunks of the source file +** sampled at evenly spaced intervals are used to populate the hash +** table. +** +** Next we begin scanning the target file using a sliding 16-byte +** window. The hash of the 16-byte window in the target is used to +** search for a matching section in the source file. When a match +** is found, a copy command is added to the delta. An effort is +** made to extend the matching section to regions that come before +** and after the 16-byte hash window. A copy command is only issued +** if the result would use less space that just quoting the text +** literally. Literal text is added to the delta for sections that +** do not match or which can not be encoded efficiently using copy +** commands. +*/ +int delta_create( + const char *zSrc, /* The source or pattern file */ + unsigned int lenSrc, /* Length of the source file */ + const char *zOut, /* The target file */ + unsigned int lenOut, /* Length of the target file */ + char *zDelta /* Write the delta into this buffer */ +){ + int i, base; + char *zOrigDelta = zDelta; + hash h; + int nHash; /* Number of hash table entries */ + int *landmark; /* Primary hash table */ + int *collide; /* Collision chain */ + int lastRead = -1; /* Last byte of zSrc read by a COPY command */ + + /* Add the target file size to the beginning of the delta + */ + putInt(lenOut, &zDelta); + *(zDelta++) = '\n'; + + /* If the source file is very small, it means that we have no + ** chance of ever doing a copy command. Just output a single + ** literal segment for the entire target and exit. + */ + if( lenSrc<=NHASH ){ + putInt(lenOut, &zDelta); + *(zDelta++) = ':'; + memcpy(zDelta, zOut, lenOut); + zDelta += lenOut; + putInt(checksum(zOut, lenOut), &zDelta); + *(zDelta++) = ';'; + return zDelta - zOrigDelta; + } + + /* Compute the hash table used to locate matching sections in the + ** source file. + */ + nHash = lenSrc/NHASH; + collide = fossil_malloc( nHash*2*sizeof(int) ); + memset(collide, -1, nHash*2*sizeof(int)); + landmark = &collide[nHash]; + for(i=0; i<(int)lenSrc-NHASH; i+=NHASH){ + int hv = hash_once(&zSrc[i]) % nHash; + collide[i/NHASH] = landmark[hv]; + landmark[hv] = i/NHASH; + } + + /* Begin scanning the target file and generating copy commands and + ** literal sections of the delta. + */ + base = 0; /* We have already generated everything before zOut[base] */ + while( base+NHASH<(int)lenOut ){ + int iSrc, iBlock; + unsigned int bestCnt, bestOfst=0, bestLitsz=0; + hash_init(&h, &zOut[base]); + i = 0; /* Trying to match a landmark against zOut[base+i] */ + bestCnt = 0; + while( 1 ){ + int hv; + int limit = 250; + + hv = hash_32bit(&h) % nHash; + DEBUG2( printf("LOOKING: %4d [%s]\n", base+i, print16(&zOut[base+i])); ) + iBlock = landmark[hv]; + while( iBlock>=0 && (limit--)>0 ){ + /* + ** The hash window has identified a potential match against + ** landmark block iBlock. But we need to investigate further. + ** + ** Look for a region in zOut that matches zSrc. Anchor the search + ** at zSrc[iSrc] and zOut[base+i]. Do not include anything prior to + ** zOut[base] or after zOut[outLen] nor anything after zSrc[srcLen]. + ** + ** Set cnt equal to the length of the match and set ofst so that + ** zSrc[ofst] is the first element of the match. litsz is the number + ** of characters between zOut[base] and the beginning of the match. + ** sz will be the overhead (in bytes) needed to encode the copy + ** command. Only generate copy command if the overhead of the + ** copy command is less than the amount of literal text to be copied. + */ + int cnt, ofst, litsz; + int j, k, x, y; + int sz; + int limitX; + + /* Beginning at iSrc, match forwards as far as we can. j counts + ** the number of characters that match */ + iSrc = iBlock*NHASH; + y = base+i; + limitX = ( lenSrc-iSrc <= lenOut-y ) ? lenSrc : iSrc + lenOut - y; + for(x=iSrc; x<limitX; x++, y++){ + if( zSrc[x]!=zOut[y] ) break; + } + j = x - iSrc - 1; + + /* Beginning at iSrc-1, match backwards as far as we can. k counts + ** the number of characters that match */ + for(k=1; k<iSrc && k<=i; k++){ + if( zSrc[iSrc-k]!=zOut[base+i-k] ) break; + } + k--; + + /* Compute the offset and size of the matching region */ + ofst = iSrc-k; + cnt = j+k+1; + litsz = i-k; /* Number of bytes of literal text before the copy */ + DEBUG2( printf("MATCH %d bytes at %d: [%s] litsz=%d\n", + cnt, ofst, print16(&zSrc[ofst]), litsz); ) + /* sz will hold the number of bytes needed to encode the "insert" + ** command and the copy command, not counting the "insert" text */ + sz = digit_count(i-k)+digit_count(cnt)+digit_count(ofst)+3; + if( cnt>=sz && cnt>(int)bestCnt ){ + /* Remember this match only if it is the best so far and it + ** does not increase the file size */ + bestCnt = cnt; + bestOfst = iSrc-k; + bestLitsz = litsz; + DEBUG2( printf("... BEST SO FAR\n"); ) + } + + /* Check the next matching block */ + iBlock = collide[iBlock]; + } + + /* We have a copy command that does not cause the delta to be larger + ** than a literal insert. So add the copy command to the delta. + */ + if( bestCnt>0 ){ + if( bestLitsz>0 ){ + /* Add an insert command before the copy */ + putInt(bestLitsz,&zDelta); + *(zDelta++) = ':'; + memcpy(zDelta, &zOut[base], bestLitsz); + zDelta += bestLitsz; + base += bestLitsz; + DEBUG2( printf("insert %d\n", bestLitsz); ) + } + base += bestCnt; + putInt(bestCnt, &zDelta); + *(zDelta++) = '@'; + putInt(bestOfst, &zDelta); + DEBUG2( printf("copy %d bytes from %d\n", bestCnt, bestOfst); ) + *(zDelta++) = ','; + if( (int)(bestOfst + bestCnt -1) > lastRead ){ + lastRead = bestOfst + bestCnt - 1; + DEBUG2( printf("lastRead becomes %d\n", lastRead); ) + } + bestCnt = 0; + break; + } + + /* If we reach this point, it means no match is found so far */ + if( base+i+NHASH>=(int)lenOut ){ + /* We have reached the end of the file and have not found any + ** matches. Do an "insert" for everything that does not match */ + putInt(lenOut-base, &zDelta); + *(zDelta++) = ':'; + memcpy(zDelta, &zOut[base], lenOut-base); + zDelta += lenOut-base; + base = lenOut; + break; + } + + /* Advance the hash by one character. Keep looking for a match */ + hash_next(&h, zOut[base+i+NHASH]); + i++; + } + } + /* Output a final "insert" record to get all the text at the end of + ** the file that does not match anything in the source file. + */ + if( base<(int)lenOut ){ + putInt(lenOut-base, &zDelta); + *(zDelta++) = ':'; + memcpy(zDelta, &zOut[base], lenOut-base); + zDelta += lenOut-base; + } + /* Output the final checksum record. */ + putInt(checksum(zOut, lenOut), &zDelta); + *(zDelta++) = ';'; + fossil_free(collide); + return zDelta - zOrigDelta; +} + +/* +** Return the size (in bytes) of the output from applying +** a delta. +** +** This routine is provided so that an procedure that is able +** to call delta_apply() can learn how much space is required +** for the output and hence allocate nor more space that is really +** needed. +*/ +int delta_output_size(const char *zDelta, int lenDelta){ + int size; + size = getInt(&zDelta, &lenDelta); + if( lenDelta<=0 || *zDelta!='\n' ){ + /* ERROR: size integer not terminated by "\n" */ + return -1; + } + return size; +} + + +/* +** Apply a delta. +** +** The output buffer should be big enough to hold the whole output +** file and a NUL terminator at the end. The delta_output_size() +** routine will determine this size for you. +** +** The delta string should be null-terminated. But the delta string +** may contain embedded NUL characters (if the input and output are +** binary files) so we also have to pass in the length of the delta in +** the lenDelta parameter. +** +** This function returns the size of the output file in bytes (excluding +** the final NUL terminator character). Except, if the delta string is +** malformed or intended for use with a source file other than zSrc, +** then this routine returns -1. +** +** Refer to the delta_create() documentation above for a description +** of the delta file format. +*/ +int delta_apply( + const char *zSrc, /* The source or pattern file */ + int lenSrc, /* Length of the source file */ + const char *zDelta, /* Delta to apply to the pattern */ + int lenDelta, /* Length of the delta */ + char *zOut /* Write the output into this preallocated buffer */ +){ + sqlite3_uint64 limit; + sqlite3_uint64 total = 0; + +#ifdef FOSSIL_ENABLE_DELTA_CKSUM_TEST + char *zOrigOut = zOut; +#endif + + limit = getInt(&zDelta, &lenDelta); + if( lenDelta<=0 || *zDelta!='\n' ){ + /* ERROR: size integer not terminated by "\n" */ + return -1; + } + zDelta++; lenDelta--; /* Skip the \n */ + while( lenDelta>0 && zDelta[0] ){ + unsigned int cnt, ofst; + cnt = getInt(&zDelta, &lenDelta); + if( lenDelta<=0 ) return -1; + switch( zDelta[0] ){ + case '@': { + zDelta++; lenDelta--; + ofst = getInt(&zDelta, &lenDelta); + if( lenDelta>0 && zDelta[0]!=',' ){ + /* ERROR: copy command not terminated by ',' */ + return -1; + } + zDelta++; lenDelta--; + DEBUG1( printf("COPY %d from %d\n", cnt, ofst); ) + total += cnt; + if( total>limit ){ + /* ERROR: copy exceeds output file size */ + return -1; + } + if( (u64)ofst+(u64)cnt > (u64)lenSrc ){ + /* ERROR: copy extends past end of input */ + return -1; + } + memcpy(zOut, &zSrc[ofst], cnt); + zOut += cnt; + break; + } + case ':': { + zDelta++; lenDelta--; + total += cnt; + if( total>limit ){ + /* ERROR: insert command gives an output larger than predicted */ + return -1; + } + DEBUG1( printf("INSERT %d\n", cnt); ) + if( cnt>lenDelta ){ + /* ERROR: insert count exceeds size of delta */ + return -1; + } + memcpy(zOut, zDelta, cnt); + zOut += cnt; + zDelta += cnt; + lenDelta -= cnt; + break; + } + case ';': { + zDelta++; lenDelta--; + zOut[0] = 0; +#ifdef FOSSIL_ENABLE_DELTA_CKSUM_TEST + if( cnt!=checksum(zOrigOut, total) ){ + /* ERROR: bad checksum */ + return -1; + } +#endif + if( total!=limit ){ + /* ERROR: generated size does not match predicted size */ + return -1; + } + return total; + } + default: { + /* ERROR: unknown delta operator */ + return -1; + } + } + } + /* ERROR: unterminated delta */ + return -1; +} + +/* +** Analyze a delta. Figure out the total number of bytes copied from +** source to target, and the total number of bytes inserted by the delta, +** and return both numbers. +*/ +int delta_analyze( + const char *zDelta, /* Delta to apply to the pattern */ + int lenDelta, /* Length of the delta */ + int *pnCopy, /* OUT: Number of bytes copied */ + int *pnInsert /* OUT: Number of bytes inserted */ +){ + unsigned int nInsert = 0; + unsigned int nCopy = 0; + + (void)getInt(&zDelta, &lenDelta); + if( lenDelta<=0 || *zDelta!='\n' ){ + /* ERROR: size integer not terminated by "\n" */ + return -1; + } + zDelta++; lenDelta--; + while( lenDelta>0 && *zDelta ){ + unsigned int cnt; + cnt = getInt(&zDelta, &lenDelta); + if( lenDelta<=0 ) break; + switch( zDelta[0] ){ + case '@': { + zDelta++; lenDelta--; + (void)getInt(&zDelta, &lenDelta); + if( lenDelta<=0 || zDelta[0]!=',' ){ + /* ERROR: copy command not terminated by ',' */ + return -1; + } + zDelta++; lenDelta--; + nCopy += cnt; + break; + } + case ':': { + zDelta++; lenDelta--; + nInsert += cnt; + if( (i64)cnt>(i64)lenDelta ){ + /* ERROR: insert count exceeds size of delta */ + return -1; + } + zDelta += cnt; + lenDelta -= cnt; + break; + } + case ';': { + *pnCopy = nCopy; + *pnInsert = nInsert; + return 0; + } + default: { + /* ERROR: unknown delta operator */ + return -1; + } + } + } + /* ERROR: unterminated delta */ + return -1; +} diff --git a/src/deltacmd.c b/src/deltacmd.c new file mode 100644 index 0000000..98ddad0 --- /dev/null +++ b/src/deltacmd.c @@ -0,0 +1,198 @@ +/* +** Copyright (c) 2006 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) + +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This module implements the interface to the delta generator. +*/ +#include "config.h" +#include "deltacmd.h" + +/* +** Create a delta that describes the change from pOriginal to pTarget +** and put that delta in pDelta. The pDelta blob is assumed to be +** uninitialized. +*/ +int blob_delta_create(Blob *pOriginal, Blob *pTarget, Blob *pDelta){ + const char *zOrig, *zTarg; + int lenOrig, lenTarg; + int len; + char *zRes; + blob_zero(pDelta); + zOrig = blob_materialize(pOriginal); + lenOrig = blob_size(pOriginal); + zTarg = blob_materialize(pTarget); + lenTarg = blob_size(pTarget); + blob_resize(pDelta, lenTarg+16); + zRes = blob_materialize(pDelta); + len = delta_create(zOrig, lenOrig, zTarg, lenTarg, zRes); + blob_resize(pDelta, len); + return 0; +} + +/* +** COMMAND: test-delta-create +** +** Usage: %fossil test-delta-create FILE1 FILE2 DELTA +** +** Create and output a delta that carries FILE1 into FILE2. +** Store the result in DELTA. +*/ +void delta_create_cmd(void){ + Blob orig, target, delta; + if( g.argc!=5 ){ + usage("ORIGIN TARGET DELTA"); + } + if( blob_read_from_file(&orig, g.argv[2], ExtFILE)<0 ){ + fossil_fatal("cannot read %s", g.argv[2]); + } + if( blob_read_from_file(&target, g.argv[3], ExtFILE)<0 ){ + fossil_fatal("cannot read %s", g.argv[3]); + } + blob_delta_create(&orig, &target, &delta); + if( blob_write_to_file(&delta, g.argv[4])<(int)blob_size(&delta) ){ + fossil_fatal("cannot write %s", g.argv[4]); + } + blob_reset(&orig); + blob_reset(&target); + blob_reset(&delta); +} + +/* +** COMMAND: test-delta-analyze +** +** Usage: %fossil test-delta-analyze FILE1 FILE2 +** +** Create and a delta that carries FILE1 into FILE2. Print the +** number bytes copied and the number of bytes inserted. +*/ +void delta_analyze_cmd(void){ + Blob orig, target, delta; + int nCopy = 0; + int nInsert = 0; + int sz1, sz2, sz3; + if( g.argc!=4 ){ + usage("ORIGIN TARGET"); + } + if( blob_read_from_file(&orig, g.argv[2], ExtFILE)<0 ){ + fossil_fatal("cannot read %s", g.argv[2]); + } + if( blob_read_from_file(&target, g.argv[3], ExtFILE)<0 ){ + fossil_fatal("cannot read %s", g.argv[3]); + } + blob_delta_create(&orig, &target, &delta); + delta_analyze(blob_buffer(&delta), blob_size(&delta), &nCopy, &nInsert); + sz1 = blob_size(&orig); + sz2 = blob_size(&target); + sz3 = blob_size(&delta); + blob_reset(&orig); + blob_reset(&target); + blob_reset(&delta); + fossil_print("original size: %8d\n", sz1); + fossil_print("bytes copied: %8d (%.2f%% of target)\n", + nCopy, (100.0*nCopy)/sz2); + fossil_print("bytes inserted: %8d (%.2f%% of target)\n", + nInsert, (100.0*nInsert)/sz2); + fossil_print("final size: %8d\n", sz2); + fossil_print("delta size: %8d\n", sz3); +} + +/* +** Apply the delta in pDelta to the original file pOriginal to generate +** the target file pTarget. The pTarget blob is initialized by this +** routine. +** +** It works ok for pTarget and pOriginal to be the same blob. +** +** Return the length of the target. Return -1 if there is an error. +*/ +int blob_delta_apply(Blob *pOriginal, Blob *pDelta, Blob *pTarget){ + int len, n; + Blob out; + + n = delta_output_size(blob_buffer(pDelta), blob_size(pDelta)); + blob_zero(&out); + if( n<0 ) return -1; + blob_resize(&out, n); + len = delta_apply( + blob_buffer(pOriginal), blob_size(pOriginal), + blob_buffer(pDelta), blob_size(pDelta), + blob_buffer(&out)); + if( len<0 ){ + blob_reset(&out); + }else if( len!=n ){ + blob_resize(&out, len); + } + if( pTarget==pOriginal ){ + blob_reset(pOriginal); + } + *pTarget = out; + return len; +} + +/* +** COMMAND: test-delta-apply +** +** Usage: %fossil test-delta-apply FILE1 DELTA FILE2 +** +** Apply DELTA to FILE1 and output the result in FILE2. +*/ +void delta_apply_cmd(void){ + Blob orig, target, delta; + if( g.argc!=5 ){ + usage("ORIGIN DELTA TARGET"); + } + if( blob_read_from_file(&orig, g.argv[2], ExtFILE)<0 ){ + fossil_fatal("cannot read %s", g.argv[2]); + } + if( blob_read_from_file(&delta, g.argv[3], ExtFILE)<0 ){ + fossil_fatal("cannot read %s", g.argv[3]); + } + blob_init(&target, 0, 0); + blob_delta_apply(&orig, &delta, &target); + if( blob_write_to_file(&target, g.argv[4])<(int)blob_size(&target) ){ + fossil_fatal("cannot write %s", g.argv[4]); + } + blob_reset(&orig); + blob_reset(&target); + blob_reset(&delta); +} + + +/* +** COMMAND: test-delta +** +** Usage: %fossil test-delta FILE1 FILE2 +** +** Read two files named on the command-line. Create and apply deltas +** going in both directions. Verify that the original files are +** correctly recovered. +*/ +void cmd_test_delta(void){ + Blob f1, f2; /* Original file content */ + Blob d12, d21; /* Deltas from f1->f2 and f2->f1 */ + Blob a1, a2; /* Recovered file content */ + if( g.argc!=4 ) usage("FILE1 FILE2"); + blob_read_from_file(&f1, g.argv[2], ExtFILE); + blob_read_from_file(&f2, g.argv[3], ExtFILE); + blob_delta_create(&f1, &f2, &d12); + blob_delta_create(&f2, &f1, &d21); + blob_delta_apply(&f1, &d12, &a2); + blob_delta_apply(&f2, &d21, &a1); + if( blob_compare(&f1,&a1) || blob_compare(&f2, &a2) ){ + fossil_fatal("delta test failed"); + } + fossil_print("ok\n"); +} diff --git a/src/deltafunc.c b/src/deltafunc.c new file mode 100644 index 0000000..88eaab9 --- /dev/null +++ b/src/deltafunc.c @@ -0,0 +1,530 @@ +/* +** Copyright (c) 2019 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This module implements SQL interfaces to the delta logic. The code +** here is adapted from the ext/misc/fossildelta.c extension in SQLite. +*/ +#include "config.h" +#include "deltafunc.h" + +/* +** SQL functions: delta_create(X,Y) +** +** Return a delta that will transform X into Y. +*/ +static void deltaCreateFunc( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char *aOrig; int nOrig; /* old blob */ + const char *aNew; int nNew; /* new blob */ + char *aOut; int nOut; /* output delta */ + + assert( argc==2 ); + if( sqlite3_value_type(argv[0])==SQLITE_NULL ) return; + if( sqlite3_value_type(argv[1])==SQLITE_NULL ) return; + nOrig = sqlite3_value_bytes(argv[0]); + aOrig = (const char*)sqlite3_value_blob(argv[0]); + nNew = sqlite3_value_bytes(argv[1]); + aNew = (const char*)sqlite3_value_blob(argv[1]); + aOut = sqlite3_malloc64(nNew+70); + if( aOut==0 ){ + sqlite3_result_error_nomem(context); + }else{ + nOut = delta_create(aOrig, nOrig, aNew, nNew, aOut); + if( nOut<0 ){ + sqlite3_free(aOut); + sqlite3_result_error(context, "cannot create fossil delta", -1); + }else{ + sqlite3_result_blob(context, aOut, nOut, sqlite3_free); + } + } +} + +/* +** SQL functions: delta_apply(X,D) +** +** Return the result of applying delta D to input X. +*/ +static void deltaApplyFunc( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char *aOrig; int nOrig; /* The X input */ + const char *aDelta; int nDelta; /* The input delta (D) */ + char *aOut; int nOut, nOut2; /* The output */ + + assert( argc==2 ); + if( sqlite3_value_type(argv[0])==SQLITE_NULL ) return; + if( sqlite3_value_type(argv[1])==SQLITE_NULL ) return; + nOrig = sqlite3_value_bytes(argv[0]); + aOrig = (const char*)sqlite3_value_blob(argv[0]); + nDelta = sqlite3_value_bytes(argv[1]); + aDelta = (const char*)sqlite3_value_blob(argv[1]); + + /* Figure out the size of the output */ + nOut = delta_output_size(aDelta, nDelta); + if( nOut<0 ){ + sqlite3_result_error(context, "corrupt fossil delta", -1); + return; + } + aOut = sqlite3_malloc64((sqlite3_int64)nOut+1); + if( aOut==0 ){ + sqlite3_result_error_nomem(context); + }else{ + nOut2 = delta_apply(aOrig, nOrig, aDelta, nDelta, aOut); + if( nOut2!=nOut ){ + sqlite3_free(aOut); + sqlite3_result_error(context, "corrupt fossil delta", -1); + }else{ + sqlite3_result_blob(context, aOut, nOut, sqlite3_free); + } + } +} + + +/* +** SQL functions: delta_output_size(D) +** +** Return the size of the output that results from applying delta D. +*/ +static void deltaOutputSizeFunc( + sqlite3_context *context, + int argc, + sqlite3_value **argv +){ + const char *aDelta; int nDelta; /* The input delta (D) */ + int nOut; /* Size of output */ + assert( argc==1 ); + if( sqlite3_value_type(argv[0])==SQLITE_NULL ) return; + nDelta = sqlite3_value_bytes(argv[0]); + aDelta = (const char*)sqlite3_value_blob(argv[0]); + + /* Figure out the size of the output */ + nOut = delta_output_size(aDelta, nDelta); + if( nOut<0 ){ + sqlite3_result_error(context, "corrupt fossil delta", -1); + return; + }else{ + sqlite3_result_int(context, nOut); + } +} + +/***************************************************************************** +** Table-valued SQL function: delta_parse(DELTA) +** +** Schema: +** +** CREATE TABLE delta_parse( +** op TEXT, +** a1 INT, +** a2 ANY, +** delta HIDDEN BLOB +** ); +** +** Given an input DELTA, this function parses the delta and returns +** rows for each entry in the delta. The op column has one of the +** values SIZE, COPY, INSERT, CHECKSUM, ERROR. +** +** Assuming no errors, the first row has op='SIZE'. a1 is the size of +** the output in bytes and a2 is NULL. +** +** After the initial SIZE row, there are zero or more 'COPY' and/or 'INSERT' +** rows. A COPY row means content is copied from the source into the +** output. Column a1 is the number of bytes to copy and a2 is the offset +** into source from which to begin copying. An INSERT row means to +** insert text into the output stream. Column a1 is the number of bytes +** to insert and column is a BLOB that contains the text to be inserted. +** +** The last row of a well-formed delta will have an op value of 'CHECKSUM'. +** The a1 column will be the value of the checksum and a2 will be NULL. +** +** If the input delta is not well-formed, then a row with an op value +** of 'ERROR' is returned. The a1 value of the ERROR row is the offset +** into the delta where the error was encountered and a2 is NULL. +*/ +typedef struct deltaparsevtab_vtab deltaparsevtab_vtab; +typedef struct deltaparsevtab_cursor deltaparsevtab_cursor; +struct deltaparsevtab_vtab { + sqlite3_vtab base; /* Base class - must be first */ + /* No additional information needed */ +}; +struct deltaparsevtab_cursor { + sqlite3_vtab_cursor base; /* Base class - must be first */ + char *aDelta; /* The delta being parsed */ + i64 iCursor; /* Current cursor location */ + i64 iNext; /* Next cursor value */ + i64 nDelta; /* Number of bytes in the delta */ + int eOp; /* Name of current operator */ + unsigned int a1, a2; /* Arguments to current operator */ +}; + +/* Operator names: +*/ +static const char *const azOp[] = { + "SIZE", "COPY", "INSERT", "CHECKSUM", "ERROR", "EOF" +}; +#define DELTAPARSE_OP_SIZE 0 +#define DELTAPARSE_OP_COPY 1 +#define DELTAPARSE_OP_INSERT 2 +#define DELTAPARSE_OP_CHECKSUM 3 +#define DELTAPARSE_OP_ERROR 4 +#define DELTAPARSE_OP_EOF 5 + +/* +** Read bytes from *pz and convert them into a positive integer. When +** finished, leave *pz pointing to the first character past the end of +** the integer. The *pLen parameter holds the length of the string +** in *pz and is decremented once for each character in the integer. +*/ +static unsigned int deltaGetInt(const char **pz, int *pLen){ + static const signed char zValue[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, + -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, + }; + unsigned int v = 0; + int c; + unsigned char *z = (unsigned char*)*pz; + unsigned char *zStart = z; + while( (c = zValue[0x7f&*(z++)])>=0 ){ + v = (v<<6) + c; + } + z--; + *pLen -= z - zStart; + *pz = (char*)z; + return v; +} + +/* +** The deltaparsevtabConnect() method is invoked to create a new +** deltaparse virtual table. +** +** Think of this routine as the constructor for deltaparsevtab_vtab objects. +** +** All this routine needs to do is: +** +** (1) Allocate the deltaparsevtab_vtab object and initialize all fields. +** +** (2) Tell SQLite (via the sqlite3_declare_vtab() interface) what the +** result set of queries against the virtual table will look like. +*/ +static int deltaparsevtabConnect( + sqlite3 *db, + void *pAux, + int argc, const char *const*argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + deltaparsevtab_vtab *pNew; + int rc; + + rc = sqlite3_declare_vtab(db, + "CREATE TABLE x(op,a1,a2,delta HIDDEN)" + ); + /* For convenience, define symbolic names for the index to each column. */ +#define DELTAPARSEVTAB_OP 0 +#define DELTAPARSEVTAB_A1 1 +#define DELTAPARSEVTAB_A2 2 +#define DELTAPARSEVTAB_DELTA 3 + if( rc==SQLITE_OK ){ + pNew = sqlite3_malloc64( sizeof(*pNew) ); + *ppVtab = (sqlite3_vtab*)pNew; + if( pNew==0 ) return SQLITE_NOMEM; + memset(pNew, 0, sizeof(*pNew)); + sqlite3_vtab_config(db, SQLITE_VTAB_INNOCUOUS); + } + return rc; +} + +/* +** This method is the destructor for deltaparsevtab_vtab objects. +*/ +static int deltaparsevtabDisconnect(sqlite3_vtab *pVtab){ + deltaparsevtab_vtab *p = (deltaparsevtab_vtab*)pVtab; + sqlite3_free(p); + return SQLITE_OK; +} + +/* +** Constructor for a new deltaparsevtab_cursor object. +*/ +static int deltaparsevtabOpen(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor){ + deltaparsevtab_cursor *pCur; + pCur = sqlite3_malloc64( sizeof(*pCur) ); + if( pCur==0 ) return SQLITE_NOMEM; + memset(pCur, 0, sizeof(*pCur)); + *ppCursor = &pCur->base; + return SQLITE_OK; +} + +/* +** Destructor for a deltaparsevtab_cursor. +*/ +static int deltaparsevtabClose(sqlite3_vtab_cursor *cur){ + deltaparsevtab_cursor *pCur = (deltaparsevtab_cursor*)cur; + sqlite3_free(pCur->aDelta); + sqlite3_free(pCur); + return SQLITE_OK; +} + + +/* +** Advance a deltaparsevtab_cursor to its next row of output. +*/ +static int deltaparsevtabNext(sqlite3_vtab_cursor *cur){ + deltaparsevtab_cursor *pCur = (deltaparsevtab_cursor*)cur; + const char *z; + int i = 0; + + pCur->iCursor = pCur->iNext; + if( pCur->iCursor >= pCur->nDelta ){ + pCur->eOp = DELTAPARSE_OP_ERROR; + pCur->iNext = pCur->nDelta; + return SQLITE_OK; + } + z = pCur->aDelta + pCur->iCursor; + pCur->a1 = deltaGetInt(&z, &i); + switch( z[0] ){ + case '@': { + z++; + if( pCur->iNext>=pCur->nDelta ){ + pCur->eOp = DELTAPARSE_OP_ERROR; + pCur->iNext = pCur->nDelta; + break; + } + pCur->a2 = deltaGetInt(&z, &i); + pCur->eOp = DELTAPARSE_OP_COPY; + pCur->iNext = (i64)(&z[1] - pCur->aDelta); + break; + } + case ':': { + z++; + pCur->a2 = (unsigned int)(z - pCur->aDelta); + pCur->eOp = DELTAPARSE_OP_INSERT; + pCur->iNext = (i64)(&z[pCur->a1] - pCur->aDelta); + break; + } + case ';': { + pCur->eOp = DELTAPARSE_OP_CHECKSUM; + pCur->iNext = pCur->nDelta; + break; + } + default: { + if( pCur->iNext==pCur->nDelta ){ + pCur->eOp = DELTAPARSE_OP_EOF; + }else{ + pCur->eOp = DELTAPARSE_OP_ERROR; + pCur->iNext = pCur->nDelta; + } + break; + } + } + return SQLITE_OK; +} + +/* +** Return values of columns for the row at which the deltaparsevtab_cursor +** is currently pointing. +*/ +static int deltaparsevtabColumn( + sqlite3_vtab_cursor *cur, /* The cursor */ + sqlite3_context *ctx, /* First argument to sqlite3_result_...() */ + int i /* Which column to return */ +){ + deltaparsevtab_cursor *pCur = (deltaparsevtab_cursor*)cur; + switch( i ){ + case DELTAPARSEVTAB_OP: { + sqlite3_result_text(ctx, azOp[pCur->eOp], -1, SQLITE_STATIC); + break; + } + case DELTAPARSEVTAB_A1: { + sqlite3_result_int(ctx, pCur->a1); + break; + } + case DELTAPARSEVTAB_A2: { + if( pCur->eOp==DELTAPARSE_OP_COPY ){ + sqlite3_result_int(ctx, pCur->a2); + }else if( pCur->eOp==DELTAPARSE_OP_INSERT ){ + if( (i64)pCur->a2 + (i64)pCur->a1 > pCur->nDelta ){ + sqlite3_result_zeroblob(ctx, pCur->a1); + }else{ + sqlite3_result_blob(ctx, pCur->aDelta+pCur->a2, pCur->a1, + SQLITE_TRANSIENT); + } + } + break; + } + case DELTAPARSEVTAB_DELTA: { + sqlite3_result_blob(ctx, pCur->aDelta, pCur->nDelta, SQLITE_TRANSIENT); + break; + } + } + return SQLITE_OK; +} + +/* +** Return the rowid for the current row. In this implementation, the +** rowid is the same as the output value. +*/ +static int deltaparsevtabRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ + deltaparsevtab_cursor *pCur = (deltaparsevtab_cursor*)cur; + *pRowid = pCur->iCursor; + return SQLITE_OK; +} + +/* +** Return TRUE if the cursor has been moved off of the last +** row of output. +*/ +static int deltaparsevtabEof(sqlite3_vtab_cursor *cur){ + deltaparsevtab_cursor *pCur = (deltaparsevtab_cursor*)cur; + return pCur->eOp==DELTAPARSE_OP_EOF || pCur->iCursor>=pCur->nDelta; +} + +/* +** This method is called to "rewind" the deltaparsevtab_cursor object back +** to the first row of output. This method is always called at least +** once prior to any call to deltaparsevtabColumn() or deltaparsevtabRowid() or +** deltaparsevtabEof(). +*/ +static int deltaparsevtabFilter( + sqlite3_vtab_cursor *pVtabCursor, + int idxNum, const char *idxStr, + int argc, sqlite3_value **argv +){ + deltaparsevtab_cursor *pCur = (deltaparsevtab_cursor *)pVtabCursor; + const char *a; + int i = 0; + pCur->eOp = DELTAPARSE_OP_ERROR; + if( idxNum!=1 ){ + return SQLITE_OK; + } + pCur->nDelta = sqlite3_value_bytes(argv[0]); + a = (const char*)sqlite3_value_blob(argv[0]); + if( pCur->nDelta==0 || a==0 ){ + return SQLITE_OK; + } + pCur->aDelta = sqlite3_malloc64( pCur->nDelta+1 ); + if( pCur->aDelta==0 ){ + pCur->nDelta = 0; + return SQLITE_NOMEM; + } + memcpy(pCur->aDelta, a, pCur->nDelta); + pCur->aDelta[pCur->nDelta] = 0; + a = pCur->aDelta; + pCur->eOp = DELTAPARSE_OP_SIZE; + pCur->a1 = deltaGetInt(&a, &i); + if( a[0]!='\n' ){ + pCur->eOp = DELTAPARSE_OP_ERROR; + pCur->a1 = pCur->a2 = 0; + pCur->iNext = pCur->nDelta; + return SQLITE_OK; + } + a++; + pCur->iNext = (i64)(a - pCur->aDelta); + return SQLITE_OK; +} + +/* +** SQLite will invoke this method one or more times while planning a query +** that uses the virtual table. This routine needs to create +** a query plan for each invocation and compute an estimated cost for that +** plan. +*/ +static int deltaparsevtabBestIndex( + sqlite3_vtab *tab, + sqlite3_index_info *pIdxInfo +){ + int i; + for(i=0; i<pIdxInfo->nConstraint; i++){ + if( pIdxInfo->aConstraint[i].iColumn != DELTAPARSEVTAB_DELTA ) continue; + if( pIdxInfo->aConstraint[i].usable==0 ) continue; + if( pIdxInfo->aConstraint[i].op!=SQLITE_INDEX_CONSTRAINT_EQ ) continue; + pIdxInfo->aConstraintUsage[i].argvIndex = 1; + pIdxInfo->aConstraintUsage[i].omit = 1; + pIdxInfo->estimatedCost = (double)1; + pIdxInfo->estimatedRows = 10; + pIdxInfo->idxNum = 1; + return SQLITE_OK; + } + pIdxInfo->idxNum = 0; + pIdxInfo->estimatedCost = (double)0x7fffffff; + pIdxInfo->estimatedRows = 0x7fffffff; + return SQLITE_CONSTRAINT; +} + +/* +** This following structure defines all the methods for the +** virtual table. +*/ +static sqlite3_module deltaparsevtabModule = { + /* iVersion */ 0, + /* xCreate */ 0, + /* xConnect */ deltaparsevtabConnect, + /* xBestIndex */ deltaparsevtabBestIndex, + /* xDisconnect */ deltaparsevtabDisconnect, + /* xDestroy */ 0, + /* xOpen */ deltaparsevtabOpen, + /* xClose */ deltaparsevtabClose, + /* xFilter */ deltaparsevtabFilter, + /* xNext */ deltaparsevtabNext, + /* xEof */ deltaparsevtabEof, + /* xColumn */ deltaparsevtabColumn, + /* xRowid */ deltaparsevtabRowid, + /* xUpdate */ 0, + /* xBegin */ 0, + /* xSync */ 0, + /* xCommit */ 0, + /* xRollback */ 0, + /* xFindMethod */ 0, + /* xRename */ 0, + /* xSavepoint */ 0, + /* xRelease */ 0, + /* xRollbackTo */ 0, + /* xShadowName */ 0, + /* xIntegrity */ 0 +}; + +/* +** Invoke this routine to register the various delta functions. +*/ +int deltafunc_init(sqlite3 *db){ + static const int enc = SQLITE_UTF8|SQLITE_INNOCUOUS; + int rc = SQLITE_OK; + rc = sqlite3_create_function(db, "delta_create", 2, enc, 0, + deltaCreateFunc, 0, 0); + if( rc==SQLITE_OK ){ + rc = sqlite3_create_function(db, "delta_apply", 2, enc, 0, + deltaApplyFunc, 0, 0); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_create_function(db, "delta_output_size", 1, enc, 0, + deltaOutputSizeFunc, 0, 0); + } + if( rc==SQLITE_OK ){ + rc = sqlite3_create_module(db, "delta_parse", &deltaparsevtabModule, 0); + } + return rc; +} diff --git a/src/descendants.c b/src/descendants.c new file mode 100644 index 0000000..7702b0c --- /dev/null +++ b/src/descendants.c @@ -0,0 +1,725 @@ +/* +** Copyright (c) 2007 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) + +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file contains code used to find descendants of a version +** or leaves of a version tree. +*/ +#include "config.h" +#include "descendants.h" +#include <assert.h> + + +/* +** Create a temporary table named "leaves" if it does not +** already exist. Load this table with the RID of all +** check-ins that are leaves which are descended from +** check-in iBase. +** +** A "leaf" is a check-in that has no children in the same branch. +** There is a separate permanent table LEAF that contains all leaves +** in the tree. This routine is used to compute a subset of that +** table consisting of leaves that are descended from a single check-in. +** +** The closeMode flag determines behavior associated with the "closed" +** tag: +** +** closeMode==0 Show all leaves regardless of the "closed" tag. +** +** closeMode==1 Show only leaves without the "closed" tag. +** +** closeMode==2 Show only leaves with the "closed" tag. +** +** The default behavior is to ignore closed leaves (closeMode==0). To +** Show all leaves, use closeMode==1. To show only closed leaves, use +** closeMode==2. +*/ +void compute_leaves(int iBase, int closeMode){ + + /* Create the LEAVES table if it does not already exist. Make sure + ** it is empty. + */ + db_multi_exec( + "CREATE TEMP TABLE IF NOT EXISTS leaves(" + " rid INTEGER PRIMARY KEY" + ");" + "DELETE FROM leaves;" + ); + + if( iBase>0 ){ + Bag seen; /* Descendants seen */ + Bag pending; /* Unpropagated descendants */ + Stmt q1; /* Query to find children of a check-in */ + Stmt isBr; /* Query to check to see if a check-in starts a new branch */ + Stmt ins; /* INSERT statement for a new record */ + + /* Initialize the bags. */ + bag_init(&seen); + bag_init(&pending); + bag_insert(&pending, iBase); + + /* This query returns all non-branch-merge children of check-in :rid. + ** + ** If a child is a merge of a fork within the same branch, it is + ** returned. Only merge children in different branches are excluded. + */ + db_prepare(&q1, + "SELECT cid FROM plink" + " WHERE pid=:rid" + " AND (isprim" + " OR coalesce((SELECT value FROM tagxref" + " WHERE tagid=%d AND rid=plink.pid), 'trunk')" + "=coalesce((SELECT value FROM tagxref" + " WHERE tagid=%d AND rid=plink.cid), 'trunk'))", + TAG_BRANCH, TAG_BRANCH + ); + + /* This query returns a single row if check-in :rid is the first + ** check-in of a new branch. + */ + db_prepare(&isBr, + "SELECT 1 FROM tagxref" + " WHERE rid=:rid AND tagid=%d AND tagtype=2" + " AND srcid>0", + TAG_BRANCH + ); + + /* This statement inserts check-in :rid into the LEAVES table. + */ + db_prepare(&ins, "INSERT OR IGNORE INTO leaves VALUES(:rid)"); + + while( bag_count(&pending) ){ + int rid = bag_first(&pending); + int cnt = 0; + bag_remove(&pending, rid); + db_bind_int(&q1, ":rid", rid); + while( db_step(&q1)==SQLITE_ROW ){ + int cid = db_column_int(&q1, 0); + if( bag_insert(&seen, cid) ){ + bag_insert(&pending, cid); + } + db_bind_int(&isBr, ":rid", cid); + if( db_step(&isBr)==SQLITE_DONE ){ + cnt++; + } + db_reset(&isBr); + } + db_reset(&q1); + if( cnt==0 && !is_a_leaf(rid) ){ + cnt++; + } + if( cnt==0 ){ + db_bind_int(&ins, ":rid", rid); + db_step(&ins); + db_reset(&ins); + } + } + db_finalize(&ins); + db_finalize(&isBr); + db_finalize(&q1); + bag_clear(&pending); + bag_clear(&seen); + }else{ + db_multi_exec( + "INSERT INTO leaves" + " SELECT leaf.rid FROM leaf" + ); + } + if( closeMode==1 ){ + db_multi_exec( + "DELETE FROM leaves WHERE rid IN" + " (SELECT leaves.rid FROM leaves, tagxref" + " WHERE tagxref.rid=leaves.rid " + " AND tagxref.tagid=%d" + " AND tagxref.tagtype>0)", + TAG_CLOSED + ); + }else if( closeMode==2 ){ + db_multi_exec( + "DELETE FROM leaves WHERE rid NOT IN" + " (SELECT leaves.rid FROM leaves, tagxref" + " WHERE tagxref.rid=leaves.rid " + " AND tagxref.tagid=%d" + " AND tagxref.tagtype>0)", + TAG_CLOSED + ); + } +} + +/* +** If RID refers to a check-in, return the mtime of that check-in - the +** Julian day number of when the check-in occurred. +*/ +double mtime_of_rid(int rid, double mtime){ + static Stmt q; + db_static_prepare(&q,"SELECT mtime FROM event WHERE objid=:rid"); + db_bind_int(&q, ":rid", rid); + if( db_step(&q)==SQLITE_ROW ){ + mtime = db_column_double(&q,0); + } + db_reset(&q); + return mtime; +} + + + +/* +** Load the record ID rid and up to |N|-1 closest ancestors into +** the "ok" table. If N is zero, no limit. If ridBackTo is not zero +** then stop the search upon reaching the ancestor with rid==ridBackTo. +*/ +void compute_ancestors(int rid, int N, int directOnly, int ridBackTo){ + if( !N ){ + N = -1; + }else if( N<0 ){ + N = -N; + } + if( directOnly ){ + /* Direct mode means to show primary parents only */ + db_multi_exec( + "WITH RECURSIVE " + " ancestor(rid, mtime) AS (" + " SELECT %d, mtime FROM event WHERE objid=%d " + " UNION " + " SELECT plink.pid, event.mtime" + " FROM ancestor, plink, event" + " WHERE plink.cid=ancestor.rid" + " AND event.objid=plink.pid" + " AND plink.isPrim" + " ORDER BY mtime DESC LIMIT %d" + " )" + "INSERT INTO ok" + " SELECT rid FROM ancestor;", + rid, rid, N + ); + }else{ + /* If not in directMode, also include merge parents, including + ** cherrypick merges. Except, terminate searches at the cherrypick + ** merge parent itself. In other words, include: + ** (1) Primary parents + ** (2) Merge parents + ** (3) Cherrypick merge parents. + ** (4) All ancestores of 1 and 2 but not of 3. + */ + double rLimitMtime = 0.0; + if( ridBackTo ){ + rLimitMtime = mtime_of_rid(ridBackTo, 0.0); + } + db_multi_exec( + "WITH RECURSIVE\n" + " parent(pid,cid,isCP) AS (\n" + " SELECT plink.pid, plink.cid, 0 AS xisCP FROM plink\n" + " UNION ALL\n" + " SELECT parentid, childid, 1 FROM cherrypick WHERE NOT isExclude\n" + " ),\n" + " ancestor(rid, mtime, isCP) AS (\n" + " SELECT %d, mtime, 0 FROM event WHERE objid=%d\n" + " UNION\n" + " SELECT parent.pid, event.mtime, parent.isCP\n" + " FROM ancestor, parent, event\n" + " WHERE parent.cid=ancestor.rid\n" + " AND event.objid=parent.pid\n" + " AND NOT ancestor.isCP\n" + " AND (event.mtime>=%.17g OR parent.pid=%d)\n" + " ORDER BY mtime DESC LIMIT %d\n" + " )\n" + "INSERT OR IGNORE INTO ok SELECT rid FROM ancestor;", + rid, rid, rLimitMtime, ridBackTo, N + ); + if( ridBackTo && db_changes()>1 ){ + db_multi_exec("INSERT OR IGNORE INTO ok VALUES(%d)", ridBackTo); + } + } +} + +/* +** Compute the youngest ancestor of record ID rid that is a member of +** branch zBranch. +*/ +int compute_youngest_ancestor_in_branch(int rid, const char *zBranch){ + return db_int(0, + "WITH RECURSIVE " + " ancestor(rid, mtime) AS (" + " SELECT %d, mtime FROM event WHERE objid=%d " + " UNION " + " SELECT plink.pid, event.mtime" + " FROM ancestor, plink, event" + " WHERE plink.cid=ancestor.rid" + " AND event.objid=plink.pid" + " ORDER BY mtime DESC" + " )" + " SELECT ancestor.rid FROM ancestor" + " WHERE EXISTS(SELECT 1 FROM tagxref" + " WHERE tagid=%d AND tagxref.rid=ancestor.rid" + " AND value=%Q AND tagtype>0)" + " ORDER BY mtime DESC" + " LIMIT 1", + rid, rid, TAG_BRANCH, zBranch + ); +} + +/* +** Compute all direct ancestors (merge ancestors do not count) +** for the check-in rid and put them in a table named "ancestor". +** Label each generation with consecutive integers going backwards +** in time such that rid has the smallest generation number and the oldest +** direct ancestor as the largest generation number. +*/ +void compute_direct_ancestors(int rid){ + db_multi_exec( + "CREATE TEMP TABLE IF NOT EXISTS ancestor(rid INTEGER UNIQUE NOT NULL," + " generation INTEGER PRIMARY KEY);" + "DELETE FROM ancestor;" + "WITH RECURSIVE g(x,i) AS (" + " VALUES(%d,1)" + " UNION ALL" + " SELECT plink.pid, g.i+1 FROM plink, g" + " WHERE plink.cid=g.x AND plink.isprim)" + "INSERT INTO ancestor(rid,generation) SELECT x,i FROM g;", + rid + ); +} + +/* +** Compute the "mtime" of the file given whose blob.rid is "fid" that +** is part of check-in "vid". The mtime will be the mtime on vid or +** some ancestor of vid where fid first appears. +*/ +int mtime_of_manifest_file( + int vid, /* The check-in that contains fid */ + int fid, /* The id of the file whose check-in time is sought */ + i64 *pMTime /* Write result here */ +){ + static int prevVid = -1; + static Stmt q; + + if( prevVid!=vid ){ + prevVid = vid; + db_multi_exec("CREATE TEMP TABLE IF NOT EXISTS ok(rid INTEGER PRIMARY KEY);" + "DELETE FROM ok;"); + compute_ancestors(vid, 100000000, 1, 0); + } + db_static_prepare(&q, + "SELECT (max(event.mtime)-2440587.5)*86400 FROM mlink, event" + " WHERE mlink.mid=event.objid" + " AND +mlink.mid IN ok" + " AND mlink.fid=:fid"); + db_bind_int(&q, ":fid", fid); + if( db_step(&q)!=SQLITE_ROW ){ + db_reset(&q); + return 1; + } + *pMTime = db_column_int64(&q, 0); + db_reset(&q); + return 0; +} + +/* +** Load the record ID rid and up to |N|-1 closest descendants into +** the "ok" table. If N is zero, no limit. +*/ +void compute_descendants(int rid, int N){ + if( !N ){ + N = -1; + }else if( N<0 ){ + N = -N; + } + db_multi_exec( + "WITH RECURSIVE\n" + " dx(rid,mtime) AS (\n" + " SELECT %d, 0\n" + " UNION\n" + " SELECT plink.cid, plink.mtime FROM dx, plink\n" + " WHERE plink.pid=dx.rid\n" + " ORDER BY 2\n" + " )\n" + "INSERT OR IGNORE INTO ok SELECT rid FROM dx LIMIT %d", + rid, N + ); +} + +/* +** COMMAND: descendants* +** +** Usage: %fossil descendants ?CHECKIN? ?OPTIONS? +** +** Find all leaf descendants of the check-in specified or if the argument +** is omitted, of the check-in currently checked out. +** +** Options: +** -R|--repository REPO Extract info from repository REPO +** -W|--width N Width of lines (default is to auto-detect). +** Must be greater than 20 or else 0 for no +** limit, resulting in a one line per entry. +** +** See also: [[finfo]], [[info]], [[leaves]] +*/ +void descendants_cmd(void){ + Stmt q; + int base, width; + const char *zWidth; + + db_find_and_open_repository(0,0); + zWidth = find_option("width","W",1); + if( zWidth ){ + width = atoi(zWidth); + if( (width!=0) && (width<=20) ){ + fossil_fatal("-W|--width value must be >20 or 0"); + } + }else{ + width = -1; + } + + /* We should be done with options.. */ + verify_all_options(); + + if( g.argc==2 ){ + base = db_lget_int("checkout", 0); + }else{ + base = name_to_typed_rid(g.argv[2], "ci"); + } + if( base==0 ) return; + compute_leaves(base, 0); + db_prepare(&q, + "%s" + " AND event.objid IN (SELECT rid FROM leaves)" + " ORDER BY event.mtime DESC", + timeline_query_for_tty() + ); + print_timeline(&q, 0, width, 0, 0); + db_finalize(&q); +} + +/* +** COMMAND: leaves* +** +** Usage: %fossil leaves ?OPTIONS? +** +** Find leaves of all branches. By default show only open leaves. +** The -a|--all flag causes all leaves (closed and open) to be shown. +** The -c|--closed flag shows only closed leaves. +** +** The --recompute flag causes the content of the "leaf" table in the +** repository database to be recomputed. +** +** Options: +** -a|--all Show ALL leaves +** --bybranch Order output by branch name +** -c|--closed Show only closed leaves +** -m|--multiple Show only cases with multiple leaves on a single branch +** --recompute Recompute the "leaf" table in the repository DB +** -W|--width N Width of lines (default is to auto-detect). Must be +** more than 39 or else 0 no limit, resulting in a single +** line per entry. +** +** See also: [[descendants]], [[finfo]], [[info]], [[branch]] +*/ +void leaves_cmd(void){ + Stmt q; + Blob sql; + int showAll = find_option("all", "a", 0)!=0; + int showClosed = find_option("closed", "c", 0)!=0; + int recomputeFlag = find_option("recompute",0,0)!=0; + int byBranch = find_option("bybranch",0,0)!=0; + int multipleFlag = find_option("multiple","m",0)!=0; + const char *zWidth = find_option("width","W",1); + char *zLastBr = 0; + int n, width; + char zLineNo[10]; + const char *zMainBranch = db_main_branch(); + + if( multipleFlag ) byBranch = 1; + if( zWidth ){ + width = atoi(zWidth); + if( (width!=0) && (width<=39) ){ + fossil_fatal("-W|--width value must be >39 or 0"); + } + }else{ + width = -1; + } + db_find_and_open_repository(0,0); + + /* We should be done with options.. */ + verify_all_options(); + + if( recomputeFlag ) leaf_rebuild(); + blob_zero(&sql); + blob_append(&sql, timeline_query_for_tty(), -1); + if( !multipleFlag ){ + /* The usual case - show all leaves */ + blob_append_sql(&sql, " AND blob.rid IN leaf"); + }else{ + /* Show only leaves where two are more occur in the same branch */ + db_multi_exec( + "CREATE TEMP TABLE openLeaf(rid INTEGER PRIMARY KEY);" + "INSERT INTO openLeaf(rid)" + " SELECT rid FROM leaf" + " WHERE NOT EXISTS(" + " SELECT 1 FROM tagxref" + " WHERE tagid=%d AND tagtype>0 AND rid=leaf.rid);", + TAG_CLOSED + ); + db_multi_exec( + "CREATE TEMP TABLE ambiguousBranch(brname TEXT);" + "INSERT INTO ambiguousBranch(brname)" + " SELECT (SELECT value FROM tagxref WHERE tagid=%d AND rid=openLeaf.rid)" + " FROM openLeaf" + " GROUP BY 1 HAVING count(*)>1;", + TAG_BRANCH + ); + db_multi_exec( + "CREATE TEMP TABLE ambiguousLeaf(rid INTEGER PRIMARY KEY);\n" + "INSERT INTO ambiguousLeaf(rid)\n" + " SELECT rid FROM openLeaf\n" + " WHERE (SELECT value FROM tagxref WHERE tagid=%d AND rid=openLeaf.rid)" + " IN (SELECT brname FROM ambiguousBranch);", + TAG_BRANCH + ); + blob_append_sql(&sql, " AND blob.rid IN ambiguousLeaf"); + } + if( showClosed ){ + blob_append_sql(&sql," AND %z", leaf_is_closed_sql("blob.rid")); + }else if( !showAll ){ + blob_append_sql(&sql," AND NOT %z", leaf_is_closed_sql("blob.rid")); + } + if( byBranch ){ + db_prepare(&q, "%s ORDER BY nullif(branch,'trunk') COLLATE nocase," + " event.mtime DESC", + blob_sql_text(&sql)); + }else{ + db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql)); + } + blob_reset(&sql); + n = 0; + while( db_step(&q)==SQLITE_ROW ){ + const char *zId = db_column_text(&q, 1); + const char *zDate = db_column_text(&q, 2); + const char *zCom = db_column_text(&q, 3); + const char *zBr = db_column_text(&q, 7); + char *z = 0; + char * zBranchPoint = 0; + + if( byBranch && fossil_strcmp(zBr, zLastBr)!=0 ){ + fossil_print("*** %s ***\n", zBr); + fossil_free(zLastBr); + zLastBr = fossil_strdup(zBr); + if( multipleFlag ) n = 0; + } + n++; + sqlite3_snprintf(sizeof(zLineNo), zLineNo, "(%d)", n); + fossil_print("%6s ", zLineNo); + if(0!=fossil_strcmp(zBr,zMainBranch)){ + int ridOfRoot; + z = mprintf("root:%s", zId); + ridOfRoot = symbolic_name_to_rid(z, "ci"); + if(ridOfRoot>0){ + zBranchPoint = mprintf(" (branched from: [%.*z])", hash_digits(0), + rid_to_uuid(ridOfRoot)); + } + fossil_free(z); + } + z = mprintf("%s [%S] %s%s", zDate, zId, zCom, + zBranchPoint ? zBranchPoint : ""); + comment_print(z, zCom, 7, width, get_comment_format()); + fossil_free(z); + fossil_free(zBranchPoint); + } + fossil_free(zLastBr); + db_finalize(&q); +} + +/* +** WEBPAGE: leaves +** +** Show leaf check-ins in a timeline. By default only open leaves +** are listed. +** +** A "leaf" is a check-in with no children in the same branch. A +** "closed leaf" is a leaf that has a "closed" tag. An "open leaf" +** is a leaf without a "closed" tag. +** +** Query parameters: +** +** all Show all leaves +** closed Show only closed leaves +** ng No graph +** nohidden Hide check-ins with "hidden" tag +** onlyhidden Show only check-ins with "hidden" tag +** brbg Background color by branch name +** ubg Background color by user name +*/ +void leaves_page(void){ + Blob sql; + Stmt q; + int showAll = P("all")!=0; + int showClosed = P("closed")!=0; + int fNg = PB("ng")!=0; /* Flag for the "ng" query parameter */ + int fNoHidden = PB("nohidden")!=0; /* "nohidden" query parameter */ + int fOnlyHidden = PB("onlyhidden")!=0; /* "onlyhidden" query parameter */ + int fBrBg = PB("brbg")!=0; /* Flag for the "brbg" query parameter */ + int fUBg = PB("ubg")!=0; /* Flag for the "ubg" query parameter */ + HQuery url; /* URL to /leaves plus query parameters */ + int tmFlags; /* Timeline display flags */ + + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + url_initialize(&url, "leaves"); + if( fNg ) url_add_parameter(&url, "ng", ""); + if( fNoHidden ) url_add_parameter(&url, "nohidden", ""); + if( fOnlyHidden ) url_add_parameter(&url, "onlyhidden", ""); + if( fBrBg ) url_add_parameter(&url, "brbg", ""); + if( fUBg ) url_add_parameter(&url, "ubg", ""); + if( !showAll ){ + style_submenu_element("All", "%s", url_render(&url, "all", "", 0, 0)); + } + if( !showClosed ){ + style_submenu_element("Closed", "%s", url_render(&url, "closed", "", 0, 0)); + } + if( showClosed || showAll ){ + style_submenu_element("Open", "%s", url_render(&url, 0, 0, 0, 0)); + } + url_reset(&url); + cgi_check_for_malice(); + style_set_current_feature("leaves"); + style_header("Leaves"); + login_anonymous_available(); + timeline_ss_submenu(); +#if 0 + style_sidebox_begin("Nomenclature:", "33%"); + @ <ol> + @ <li> A <div class="sideboxDescribed">leaf</div> + @ is a check-in with no descendants in the same branch.</li> + @ <li> An <div class="sideboxDescribed">open leaf</div> + @ is a leaf that does not have a "closed" tag + @ and is thus assumed to still be in use.</li> + @ <li> A <div class="sideboxDescribed">closed leaf</div> + @ has a "closed" tag and is thus assumed to + @ be historical and no longer in active use.</li> + @ </ol> + style_sidebox_end(); +#endif + + if( showAll ){ + @ <h1>All leaves, both open and closed:</h1> + }else if( showClosed ){ + @ <h1>Closed leaves:</h1> + }else{ + @ <h1>Open leaves:</h1> + } + blob_zero(&sql); + blob_append(&sql, timeline_query_for_www(), -1); + blob_append_sql(&sql, " AND blob.rid IN leaf"); + if( showClosed ){ + blob_append_sql(&sql," AND %z", leaf_is_closed_sql("blob.rid")); + }else if( !showAll ){ + blob_append_sql(&sql," AND NOT %z", leaf_is_closed_sql("blob.rid")); + } + if( fNoHidden || fOnlyHidden ){ + const char* zUnaryOp = fNoHidden ? "NOT" : ""; + blob_append_sql(&sql, + " AND %s EXISTS(SELECT 1 FROM tagxref" + " WHERE tagid=%d AND tagtype>0 AND rid=blob.rid)\n", + zUnaryOp/*safe-for-%s*/, TAG_HIDDEN); + } + db_prepare(&q, "%s ORDER BY event.mtime DESC", blob_sql_text(&sql)); + blob_reset(&sql); + /* Always specify TIMELINE_DISJOINT, or graph_finish() may fail because of too + ** many descenders to (off-screen) parents. */ + tmFlags = TIMELINE_LEAFONLY | TIMELINE_DISJOINT | TIMELINE_NOSCROLL; + if( fNg==0 ) tmFlags |= TIMELINE_GRAPH; + if( fBrBg ) tmFlags |= TIMELINE_BRCOLOR; + if( fUBg ) tmFlags |= TIMELINE_UCOLOR; + www_print_timeline(&q, tmFlags, 0); + db_finalize(&q); + @ <br> + style_finish_page(); +} + +#if INTERFACE +/* Flag parameters to compute_uses_file() */ +#define USESFILE_DELETE 0x01 /* Include the check-ins where file deleted */ + +#endif + +/* +** Append a new VALUES term. +*/ +static void uses_file_append_term(Blob *pSql, int *pnCnt, int rid){ + if( *pnCnt==0 ){ + blob_append_sql(pSql, "(%d)", rid); + *pnCnt = 4; + }else if( (*pnCnt)%10==9 ){ + blob_append_sql(pSql, ",\n (%d)", rid); + }else{ + blob_append_sql(pSql, ",(%d)", rid); + } + ++*pnCnt; +} + + +/* +** Add to table zTab the record ID (rid) of every check-in that contains +** the file fid. +*/ +void compute_uses_file(const char *zTab, int fid, int usesFlags){ + Bag seen; + Bag pending; + Blob ins = BLOB_INITIALIZER; + int nIns = 0; + Stmt q; + int rid; + + bag_init(&seen); + bag_init(&pending); + blob_append_sql(&ins, "INSERT OR IGNORE INTO \"%w\" VALUES", zTab); + db_prepare(&q, "SELECT mid FROM mlink WHERE fid=%d", fid); + while( db_step(&q)==SQLITE_ROW ){ + int mid = db_column_int(&q, 0); + bag_insert(&pending, mid); + bag_insert(&seen, mid); + uses_file_append_term(&ins, &nIns, mid); + } + db_finalize(&q); + + db_prepare(&q, "SELECT mid FROM mlink WHERE pid=%d", fid); + while( db_step(&q)==SQLITE_ROW ){ + int mid = db_column_int(&q, 0); + bag_insert(&seen, mid); + if( usesFlags & USESFILE_DELETE ){ + uses_file_append_term(&ins, &nIns, mid); + } + } + db_finalize(&q); + db_prepare(&q, "SELECT cid FROM plink WHERE pid=:rid AND isprim"); + + while( (rid = bag_first(&pending))!=0 ){ + bag_remove(&pending, rid); + db_bind_int(&q, ":rid", rid); + while( db_step(&q)==SQLITE_ROW ){ + int mid = db_column_int(&q, 0); + if( bag_find(&seen, mid) ) continue; + bag_insert(&seen, mid); + bag_insert(&pending, mid); + uses_file_append_term(&ins, &nIns, mid); + } + db_reset(&q); + } + db_finalize(&q); + if( nIns>0 ) db_exec_sql(blob_str(&ins)); + blob_reset(&ins); + bag_clear(&seen); + bag_clear(&pending); +} diff --git a/src/diff.c b/src/diff.c new file mode 100644 index 0000000..f77afbe --- /dev/null +++ b/src/diff.c @@ -0,0 +1,4054 @@ +/* +** Copyright (c) 2007 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file contains code used to compute a "diff" between two +** text files. +*/ +#include "config.h" +#include "diff.h" +#include <assert.h> +#include <errno.h> + + +#if INTERFACE +/* +** Flag parameters to the text_diff() routine used to control the formatting +** of the diff output. +*/ +#define DIFF_IGNORE_EOLWS 0x00000001 /* Ignore end-of-line whitespace */ +#define DIFF_IGNORE_ALLWS 0x00000003 /* Ignore all whitespace */ +#define DIFF_SIDEBYSIDE 0x00000004 /* Generate a side-by-side diff */ +#define DIFF_VERBOSE 0x00000008 /* Missing shown as empty files */ +#define DIFF_BRIEF 0x00000010 /* Show filenames only */ +#define DIFF_HTML 0x00000020 /* Render for HTML */ +#define DIFF_LINENO 0x00000040 /* Show line numbers */ +#define DIFF_NUMSTAT 0x00000080 /* Show line count of changes */ +#define DIFF_NOOPT 0x00000100 /* Suppress optimizations (debug) */ +#define DIFF_INVERT 0x00000200 /* Invert the diff (debug) */ +#define DIFF_CONTEXT_EX 0x00000400 /* Use context even if zero */ +#define DIFF_NOTTOOBIG 0x00000800 /* Only display if not too big */ +#define DIFF_STRIP_EOLCR 0x00001000 /* Strip trailing CR */ +#define DIFF_SLOW_SBS 0x00002000 /* Better but slower side-by-side */ +#define DIFF_WEBPAGE 0x00004000 /* Complete webpage */ +#define DIFF_BROWSER 0x00008000 /* The --browser option */ +#define DIFF_JSON 0x00010000 /* JSON output */ +#define DIFF_DEBUG 0x00020000 /* Debugging diff output */ +#define DIFF_RAW 0x00040000 /* Raw triples - for debugging */ +#define DIFF_TCL 0x00080000 /* For the --tk option */ +#define DIFF_INCBINARY 0x00100000 /* The --diff-binary option */ +#define DIFF_SHOW_VERS 0x00200000 /* Show compared versions */ +#define DIFF_DARKMODE 0x00400000 /* Use dark mode for HTML */ +#define DIFF_BY_TOKEN 0x01000000 /* Split on tokens, not lines */ + +/* +** Per file information that may influence output. +*/ +#define DIFF_FILE_ADDED 0x40000000 /* Added or rename destination */ +#define DIFF_FILE_DELETED 0x80000000 /* Deleted or rename source */ +#define DIFF_FILE_MASK 0xc0000000 /* Used for clearing file flags */ + +/* +** These error messages are shared in multiple locations. They are defined +** here for consistency. +*/ +#define DIFF_CANNOT_COMPUTE_BINARY \ + "cannot compute difference between binary files\n" + +#define DIFF_CANNOT_COMPUTE_SYMLINK \ + "cannot compute difference between symlink and regular file\n" + +#define DIFF_TOO_MANY_CHANGES \ + "more than 10,000 changes\n" + +#define DIFF_WHITESPACE_ONLY \ + "whitespace changes only\n" + +/* +** Maximum length of a line in a text file, in bytes. (2**15 = 32768 bytes) +*/ +#define LENGTH_MASK_SZ 15 +#define LENGTH_MASK ((1<<LENGTH_MASK_SZ)-1) + +/* +** An instance of this object describes the formatting and processing +** details desired of a "diff" operation. +** +** Conceptually, this object is as an encoding of the command-line options +** for the "fossil diff" command. That is not a precise description, though, +** because not all diff operations are started from the command-line. But +** the idea is sound. +** +** Information encoded by this object includes but is not limited to: +** +** * The desired output format (unified vs. side-by-side, +** TCL, JSON, HTML vs. plain-text). +** +** * Number of lines of context surrounding each difference block +** +** * Width of output columns for text side-by-side diffop +*/ +struct DiffConfig { + u64 diffFlags; /* Diff flags */ + int nContext; /* Number of lines of context */ + int wColumn; /* Column width in -y mode */ + u32 nFile; /* Number of files diffed so far */ + const char *zDiffCmd; /* External diff command to use instead of builtin */ + const char *zBinGlob; /* GLOB pattern for binary files */ + ReCompiled *pRe; /* Show only changes matching this pattern */ + const char *zLeftHash; /* HASH-id of the left file */ + const char *azLabel[2]; /* Optional labels for left and right files */ +}; + +#endif /* INTERFACE */ + +/* +** Initialize memory for a DiffConfig based on just a diffFlags integer. +*/ +DiffConfig *diff_config_init(DiffConfig *pCfg, u64 diffFlags){ + memset(pCfg, 0, sizeof(*pCfg)); + pCfg->diffFlags = diffFlags; + return pCfg; +} + +/* +** Information about each line of a file being diffed. +** +** The lower LENGTH_MASK_SZ bits of the hash (DLine.h) are the length +** of the line. If any line is longer than LENGTH_MASK characters, +** the file is considered binary. +*/ +typedef struct DLine DLine; +struct DLine { + const char *z; /* The text of the line */ + u64 h; /* Hash of the line */ + unsigned short indent; /* Index of first non-space */ + unsigned short n; /* number of bytes */ + unsigned short nw; /* number of bytes without leading/trailing space */ + unsigned int iNext; /* 1+(Index of next line with same the same hash) */ + + /* an array of DLine elements serves two purposes. The fields + ** above are one per line of input text. But each entry is also + ** a bucket in a hash table, as follows: */ + unsigned int iHash; /* 1+(first entry in the hash chain) */ +}; + +/* +** Length of a dline +*/ +#define LENGTH(X) ((X)->n) + +/* +** Number of diff chunks generated +*/ +static int nChunk = 0; + +/* +** A context for running a raw diff. +** +** The aEdit[] array describes the raw diff. Each triple of integers in +** aEdit[] means: +** +** (1) COPY: Number of lines aFrom and aTo have in common +** (2) DELETE: Number of lines found only in aFrom +** (3) INSERT: Number of lines found only in aTo +** +** The triples repeat until all lines of both aFrom and aTo are accounted +** for. +*/ +typedef struct DContext DContext; +struct DContext { + int *aEdit; /* Array of copy/delete/insert triples */ + int nEdit; /* Number of integers (3x num of triples) in aEdit[] */ + int nEditAlloc; /* Space allocated for aEdit[] */ + DLine *aFrom; /* File on left side of the diff */ + int nFrom; /* Number of lines in aFrom[] */ + DLine *aTo; /* File on right side of the diff */ + int nTo; /* Number of lines in aTo[] */ + int (*xDiffer)(const DLine *,const DLine *); /* comparison function */ +}; + +/* Fast isspace for use by diff */ +static const char diffIsSpace[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; +#define diff_isspace(X) (diffIsSpace[(unsigned char)(X)]) + +/* +** Count the number of lines in the input string. Include the last line +** in the count even if it lacks the \n terminator. If an empty string +** is specified, the number of lines is zero. For the purposes of this +** function, a string is considered empty if it contains no characters +** -OR- it contains only NUL characters. +** +** Returns true if the input seems to be plain text input, else false. +** If it returns false, pnLine is not modified, else it is set to the +** number of lines in z. +*/ +int count_lines( + const char *z, + int n, + int *pnLine +){ + int nLine; + const char *zNL, *z2; + for(nLine=0, z2=z; (zNL = strchr(z2,'\n'))!=0; z2=zNL+1, nLine++){} + if( z2[0]!='\0' ){ + nLine++; + do{ z2++; }while( z2[0]!='\0' ); + } + if( n!=(int)(z2-z) ) return 0; + if( pnLine ) *pnLine = nLine; + return 1; +} + +/* +** Return an array of DLine objects containing a pointer to the +** start of each line and a hash of that line. The lower +** bits of the hash store the length of each line. +** +** Trailing whitespace is removed from each line. 2010-08-20: Not any +** more. If trailing whitespace is ignored, the "patch" command gets +** confused by the diff output. Ticket [a9f7b23c2e376af5b0e5b] +** +** Return 0 if the file is binary or contains a line that is +** too long. +** +** Profiling show that in most cases this routine consumes the bulk of +** the CPU time on a diff. +*/ +static DLine *break_into_lines( + const char *z, + int n, + int *pnLine, + u64 diffFlags +){ + int nLine, i, k, nn, s, x; + u64 h, h2; + DLine *a; + const char *zNL; + + if( count_lines(z, n, &nLine)==0 ){ + return 0; + } + assert( nLine>0 || z[0]=='\0' ); + a = fossil_malloc( sizeof(a[0])*nLine ); + memset(a, 0, sizeof(a[0])*nLine); + if( nLine==0 ){ + *pnLine = 0; + return a; + } + i = 0; + do{ + zNL = strchr(z,'\n'); + if( zNL==0 ) zNL = z+n; + nn = (int)(zNL - z); + if( nn>LENGTH_MASK ){ + fossil_free(a); + return 0; + } + a[i].z = z; + k = nn; + if( diffFlags & DIFF_STRIP_EOLCR ){ + if( k>0 && z[k-1]=='\r' ){ k--; } + } + a[i].n = k; + if( diffFlags & DIFF_IGNORE_EOLWS ){ + while( k>0 && diff_isspace(z[k-1]) ){ k--; } + } + if( (diffFlags & DIFF_IGNORE_ALLWS)==DIFF_IGNORE_ALLWS ){ + int numws = 0; + for(s=0; s<k && z[s]<=' '; s++){} + a[i].indent = s; + a[i].nw = k - s; + for(h=0, x=s; x<k; x++){ + char c = z[x]; + if( diff_isspace(c) ){ + ++numws; + }else{ + h = (h^c)*9000000000000000041LL; + } + } + k -= numws; + }else{ + int k2 = k & ~0x7; + u64 m; + for(h=x=s=0; x<k2; x += 8){ + memcpy(&m, z+x, 8); + h = (h^m)*9000000000000000041LL; + } + m = 0; + memcpy(&m, z+x, k-k2); + h ^= m; + } + a[i].h = h = ((h%281474976710597LL)<<LENGTH_MASK_SZ) | (k-s); + h2 = h % nLine; + a[i].iNext = a[h2].iHash; + a[h2].iHash = i+1; + z += nn+1; n -= nn+1; + i++; + }while( zNL[0]!='\0' && zNL[1]!='\0' ); + assert( i==nLine ); + + /* Return results */ + *pnLine = nLine; + return a; +} + +/* +** Character classes for the purpose of tokenization. +** +** 1 - alphanumeric +** 2 - whitespace +** 3 - punctuation +*/ +static char aTCharClass[256] = { + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, + + 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, + 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, + + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 +}; + +/* +** Count the number of tokens in the given string. +*/ +static int count_tokens(const unsigned char *p, int n){ + int nToken = 0; + int iPrev = 0; + int i; + for(i=0; i<n; i++){ + char x = aTCharClass[p[i]]; + if( x!=iPrev ){ + iPrev = x; + nToken++; + } + } + return nToken; +} + +/* +** Return an array of DLine objects containing a pointer to the +** start of each token and a hash of that token. The lower +** bits of the hash store the length of each token. +** +** This is like break_into_lines() except that it works with tokens +** instead of lines. A token is: +** +** * A contiguous sequence of alphanumeric characters. +** * A contiguous sequence of whitespace +** * A contiguous sequence of punctuation characters. +** +** Return 0 if the file is binary or contains a line that is +** too long. +*/ +static DLine *break_into_tokens( + const char *z, + int n, + int *pnToken, + u64 diffFlags +){ + int nToken, i, k; + u64 h, h2; + DLine *a; + unsigned char *p = (unsigned char*)z; + + nToken = count_tokens(p, n); + a = fossil_malloc( sizeof(a[0])*(nToken+1) ); + memset(a, 0, sizeof(a[0])*(nToken+1)); + if( n==0 ){ + *pnToken = 0; + return a; + } + i = 0; + while( n>0 ){ + char x = aTCharClass[*p]; + h = 0xcbf29ce484222325LL; + for(k=1; k<n && aTCharClass[p[k]]==x; k++){ + h ^= p[k]; + h *= 0x100000001b3LL; + } + a[i].z = (char*)p; + a[i].n = k; + a[i].h = h = ((h%281474976710597LL)<<LENGTH_MASK_SZ) | k; + h2 = h % nToken; + a[i].iNext = a[h2].iHash; + a[h2].iHash = i+1; + p += k; n -= k; + i++; + }; + assert( i==nToken ); + + /* Return results */ + *pnToken = nToken; + return a; +} + +/* +** Return zero if two DLine elements are identical. +*/ +static int compare_dline(const DLine *pA, const DLine *pB){ + if( pA->h!=pB->h ) return 1; + return memcmp(pA->z,pB->z, pA->h&LENGTH_MASK); +} + +/* +** Return zero if two DLine elements are identical, ignoring +** all whitespace. The indent field of pA/pB already points +** to the first non-space character in the string. +*/ +static int compare_dline_ignore_allws(const DLine *pA, const DLine *pB){ + if( pA->h==pB->h ){ + int a, b; + if( memcmp(pA->z, pB->z, pA->h&LENGTH_MASK)==0 ) return 0; + a = pA->indent; + b = pB->indent; + while( a<pA->n || b<pB->n ){ + if( a<pA->n && b<pB->n && pA->z[a++] != pB->z[b++] ) return 1; + while( a<pA->n && diff_isspace(pA->z[a])) ++a; + while( b<pB->n && diff_isspace(pB->z[b])) ++b; + } + return pA->n-a != pB->n-b; + } + return 1; +} + +/* +** Return true if the regular expression *pRe matches any of the +** N dlines +*/ +static int re_dline_match( + ReCompiled *pRe, /* The regular expression to be matched */ + const DLine *aDLine, /* First of N DLines to compare against */ + int N /* Number of DLines to check */ +){ + while( N-- ){ + if( re_match(pRe, (const unsigned char *)aDLine->z, LENGTH(aDLine)) ){ + return 1; + } + aDLine++; + } + return 0; +} + +/* +** Append a single line of context-diff output to pOut. +*/ +static void appendDiffLine( + Blob *pOut, /* Where to write the line of output */ + char cPrefix, /* One of " ", "+", or "-" */ + const DLine *pLine /* The line to be output */ +){ + blob_append_char(pOut, cPrefix); + blob_append(pOut, pLine->z, pLine->n); + blob_append_char(pOut, '\n'); +} + +/* +** Add two line numbers to the beginning of an output line for a context +** diff. One or the other of the two numbers might be zero, which means +** to leave that number field blank. +*/ +static void appendDiffLineno(Blob *pOut, int lnA, int lnB){ + if( lnA>0 ){ + blob_appendf(pOut, "%6d ", lnA); + }else{ + blob_append(pOut, " ", 7); + } + if( lnB>0 ){ + blob_appendf(pOut, "%6d ", lnB); + }else{ + blob_append(pOut, " ", 8); + } +} + +/* +** Output a patch-style text diff. +*/ +static void contextDiff( + DContext *p, /* The difference */ + Blob *pOut, /* Output a context diff to here */ + DiffConfig *pCfg /* Configuration options */ +){ + const DLine *A; /* Left side of the diff */ + const DLine *B; /* Right side of the diff */ + int a = 0; /* Index of next line in A[] */ + int b = 0; /* Index of next line in B[] */ + int *R; /* Array of COPY/DELETE/INSERT triples */ + int r; /* Index into R[] */ + int nr; /* Number of COPY/DELETE/INSERT triples to process */ + int mxr; /* Maximum value for r */ + int na, nb; /* Number of lines shown from A and B */ + int i, j; /* Loop counters */ + int m; /* Number of lines to output */ + int skip; /* Number of lines to skip */ + int nContext; /* Number of lines of context */ + int showLn; /* Show line numbers */ + int showDivider = 0; /* True to show the divider between diff blocks */ + + nContext = diff_context_lines(pCfg); + showLn = (pCfg->diffFlags & DIFF_LINENO)!=0; + A = p->aFrom; + B = p->aTo; + R = p->aEdit; + mxr = p->nEdit; + while( mxr>2 && R[mxr-1]==0 && R[mxr-2]==0 ){ mxr -= 3; } + for(r=0; r<mxr; r += 3*nr){ + /* Figure out how many triples to show in a single block */ + for(nr=1; 3*nr<mxr && R[r+nr*3]>0 && R[r+nr*3]<(int)nContext*2; nr++){} + /* printf("r=%d nr=%d\n", r, nr); */ + + /* For the current block comprising nr triples, figure out + ** how many lines of A and B are to be displayed + */ + if( R[r]>nContext ){ + na = nb = nContext; + skip = R[r] - nContext; + }else{ + na = nb = R[r]; + skip = 0; + } + for(i=0; i<nr; i++){ + na += R[r+i*3+1]; + nb += R[r+i*3+2]; + } + if( R[r+nr*3]>nContext ){ + na += nContext; + nb += nContext; + }else{ + na += R[r+nr*3]; + nb += R[r+nr*3]; + } + for(i=1; i<nr; i++){ + na += R[r+i*3]; + nb += R[r+i*3]; + } + + /* Show the header for this block, or if we are doing a modified + ** context diff that contains line numbers, show the separator from + ** the previous block. + */ + nChunk++; + if( showLn ){ + if( !showDivider ){ + /* Do not show a top divider */ + showDivider = 1; + }else{ + blob_appendf(pOut, "%.80c\n", '.'); + } + }else{ + /* + * If the patch changes an empty file or results in an empty file, + * the block header must use 0,0 as position indicator and not 1,0. + * Otherwise, patch would be confused and may reject the diff. + */ + blob_appendf(pOut,"@@ -%d,%d +%d,%d @@", + na ? a+skip+1 : a+skip, na, + nb ? b+skip+1 : b+skip, nb); + blob_append(pOut, "\n", 1); + } + + /* Show the initial common area */ + a += skip; + b += skip; + m = R[r] - skip; + for(j=0; j<m; j++){ + if( showLn ) appendDiffLineno(pOut, a+j+1, b+j+1); + appendDiffLine(pOut, ' ', &A[a+j]); + } + a += m; + b += m; + + /* Show the differences */ + for(i=0; i<nr; i++){ + m = R[r+i*3+1]; + for(j=0; j<m; j++){ + if( showLn ) appendDiffLineno(pOut, a+j+1, 0); + appendDiffLine(pOut, '-', &A[a+j]); + } + a += m; + m = R[r+i*3+2]; + for(j=0; j<m; j++){ + if( showLn ) appendDiffLineno(pOut, 0, b+j+1); + appendDiffLine(pOut, '+', &B[b+j]); + } + b += m; + if( i<nr-1 ){ + m = R[r+i*3+3]; + for(j=0; j<m; j++){ + if( showLn ) appendDiffLineno(pOut, a+j+1, b+j+1); + appendDiffLine(pOut, ' ', &A[a+j]); + } + b += m; + a += m; + } + } + + /* Show the final common area */ + assert( nr==i ); + m = R[r+nr*3]; + if( m>nContext ) m = nContext; + for(j=0; j<m; j++){ + if( showLn ) appendDiffLineno(pOut, a+j+1, b+j+1); + appendDiffLine(pOut, ' ', &A[a+j]); + } + } +} + +#define MX_CSN 8 /* Maximum number of change spans across a change region */ + +/* +** A description of zero or more (up to MX_CSN) areas of difference +** between two lines of text. +*/ +typedef struct LineChange LineChange; +struct LineChange { + int n; /* Number of change spans */ + struct Span { + int iStart1; /* Byte offset to start of a change on the left */ + int iLen1; /* Length of the left change in bytes */ + int iStart2; /* Byte offset to start of a change on the right */ + int iLen2; /* Length of the change on the right in bytes */ + int isMin; /* True if this change is known to have no useful subdivs */ + } a[MX_CSN]; /* Array of change spans, sorted order */ +}; + +/* +** The two text segments zLeft and zRight are known to be different on +** both ends, but they might have a common segment in the middle. If +** they do not have a common segment, return 0. If they do have a large +** common segment, return 1 and before doing so set: +** +** aLCS[0] = start of the common segment in zLeft +** aLCS[1] = end of the common segment in zLeft +** aLCS[2] = start of the common segment in zLeft +** aLCS[3] = end of the common segment in zLeft +** +** This computation is for display purposes only and does not have to be +** optimal or exact. +*/ +static int textLCS( + const char *zLeft, int nA, /* String on the left */ + const char *zRight, int nB, /* String on the right */ + int *aLCS /* Identify bounds of LCS here */ +){ + const unsigned char *zA = (const unsigned char*)zLeft; /* left string */ + const unsigned char *zB = (const unsigned char*)zRight; /* right string */ + int i, j, k; /* Loop counters */ + int lenBest = 0; /* Match length to beat */ + + for(i=0; i<nA-lenBest; i++){ + unsigned char cA = zA[i]; + if( (cA&0xc0)==0x80 ) continue; + for(j=0; j<nB-lenBest; j++ ){ + if( zB[j]==cA ){ + for(k=1; j+k<nB && i+k<nA && zB[j+k]==zA[i+k]; k++){} + while( (zB[j+k]&0xc0)==0x80 ){ k--; } + if( k>lenBest ){ + lenBest = k; + aLCS[0] = i; + aLCS[1] = i+k; + aLCS[2] = j; + aLCS[3] = j+k; + } + } + } + } + return lenBest>0; +} + +/* +** Find the smallest spans that are different between two text strings that +** are known to be different on both ends. +*/ +static int textLineChanges( + const char *zLeft, int nA, /* String on the left */ + const char *zRight, int nB, /* String on the right */ + LineChange *p /* Write results here */ +){ + p->n = 1; + p->a[0].iStart1 = 0; + p->a[0].iLen1 = nA; + p->a[0].iStart2 = 0; + p->a[0].iLen2 = nB; + p->a[0].isMin = 0; + while( p->n<MX_CSN-1 ){ + int mxi = -1; + int mxLen = -1; + int x, i; + int aLCS[4]; + struct Span *a, *b; + memset(aLCS, 0, sizeof(aLCS)); + for(i=0; i<p->n; i++){ + if( p->a[i].isMin ) continue; + x = p->a[i].iLen1; + if( p->a[i].iLen2<x ) x = p->a[i].iLen2; + if( x>mxLen ){ + mxLen = x; + mxi = i; + } + } + if( mxLen<6 ) break; + x = textLCS(zLeft + p->a[mxi].iStart1, p->a[mxi].iLen1, + zRight + p->a[mxi].iStart2, p->a[mxi].iLen2, aLCS); + if( x==0 ){ + p->a[mxi].isMin = 1; + continue; + } + a = p->a+mxi; + b = a+1; + if( mxi<p->n-1 ){ + memmove(b+1, b, sizeof(*b)*(p->n-mxi-1)); + } + p->n++; + b->iStart1 = a->iStart1 + aLCS[1]; + b->iLen1 = a->iLen1 - aLCS[1]; + a->iLen1 = aLCS[0]; + b->iStart2 = a->iStart2 + aLCS[3]; + b->iLen2 = a->iLen2 - aLCS[3]; + a->iLen2 = aLCS[2]; + b->isMin = 0; + } + return p->n; +} + +/* +** Return true if the string starts with n spaces +*/ +static int allSpaces(const char *z, int n){ + int i; + for(i=0; i<n && diff_isspace(z[i]); i++){} + return i==n; +} + +/* +** Try to improve the human-readability of the LineChange p. +** +** (1) If the first change span shows a change of indentation, try to +** move that indentation change to the left margin. +** +** (2) Try to shift changes so that they begin or end with a space. +*/ +static void improveReadability( + const char *zA, /* Left line of the change */ + const char *zB, /* Right line of the change */ + LineChange *p /* The LineChange to be adjusted */ +){ + int j, n, len; + if( p->n<1 ) return; + + /* (1) Attempt to move indentation changes to the left margin */ + if( p->a[0].iLen1==0 + && (len = p->a[0].iLen2)>0 + && (j = p->a[0].iStart2)>0 + && zB[0]==zB[j] + && allSpaces(zB, j) + ){ + for(n=1; n<len && n<j && zB[j]==zB[j+n]; n++){} + if( n<len ){ + memmove(&p->a[1], &p->a[0], sizeof(p->a[0])*p->n); + p->n++; + p->a[0] = p->a[1]; + p->a[1].iStart2 += n; + p->a[1].iLen2 -= n; + p->a[0].iLen2 = n; + } + p->a[0].iStart1 = 0; + p->a[0].iStart2 = 0; + }else + if( p->a[0].iLen2==0 + && (len = p->a[0].iLen1)>0 + && (j = p->a[0].iStart1)>0 + && zA[0]==zA[j] + && allSpaces(zA, j) + ){ + for(n=1; n<len && n<j && zA[j]==zA[j+n]; n++){} + if( n<len ){ + memmove(&p->a[1], &p->a[0], sizeof(p->a[0])*p->n); + p->n++; + p->a[0] = p->a[1]; + p->a[1].iStart1 += n; + p->a[1].iLen1 -= n; + p->a[0].iLen1 = n; + } + p->a[0].iStart1 = 0; + p->a[0].iStart2 = 0; + } + + /* (2) Try to shift changes so that they begin or end with a + ** space. (TBD) */ +} + + +/* +** Given two lines of text, pFrom and pTo, compute a set of changes +** between those two lines, for enhanced display purposes. +** +** The result is written into the LineChange object given by the +** third parameter. +*/ +static void oneLineChange( + const DLine *pLeft, /* Left line of the change */ + const DLine *pRight, /* Right line of the change */ + LineChange *p /* OUTPUT: Write the results here */ +){ + int nLeft; /* Length of left line in bytes */ + int nRight; /* Length of right line in bytes */ + int nShort; /* Shortest of left and right */ + int nPrefix; /* Length of common prefix */ + int nSuffix; /* Length of common suffix */ + int nCommon; /* Total byte length of suffix and prefix */ + const char *zLeft; /* Text of the left line */ + const char *zRight; /* Text of the right line */ + int nLeftDiff; /* nLeft - nPrefix - nSuffix */ + int nRightDiff; /* nRight - nPrefix - nSuffix */ + + nLeft = pLeft->n; + zLeft = pLeft->z; + nRight = pRight->n; + zRight = pRight->z; + nShort = nLeft<nRight ? nLeft : nRight; + + nPrefix = 0; + while( nPrefix<nShort && zLeft[nPrefix]==zRight[nPrefix] ){ + nPrefix++; + } + if( nPrefix<nShort ){ + while( nPrefix>0 && (zLeft[nPrefix]&0xc0)==0x80 ) nPrefix--; + } + nSuffix = 0; + if( nPrefix<nShort ){ + while( nSuffix<nShort + && zLeft[nLeft-nSuffix-1]==zRight[nRight-nSuffix-1] ){ + nSuffix++; + } + if( nSuffix<nShort ){ + while( nSuffix>0 && (zLeft[nLeft-nSuffix]&0xc0)==0x80 ) nSuffix--; + } + if( nSuffix==nLeft || nSuffix==nRight ) nPrefix = 0; + } + nCommon = nPrefix + nSuffix; + + /* If the prefix and suffix overlap, that means that we are dealing with + ** a pure insertion or deletion of text that can have multiple alignments. + ** Try to find an alignment to begins and ends on whitespace, or on + ** punctuation, rather than in the middle of a name or number. + */ + if( nCommon > nShort ){ + int iBest = -1; + int iBestVal = -1; + int i; + int nLong = nLeft<nRight ? nRight : nLeft; + int nGap = nLong - nShort; + for(i=nShort-nSuffix; i<=nPrefix; i++){ + int iVal = 0; + char c = zLeft[i]; + if( diff_isspace(c) ){ + iVal += 5; + }else if( !fossil_isalnum(c) ){ + iVal += 2; + } + c = zLeft[i+nGap-1]; + if( diff_isspace(c) ){ + iVal += 5; + }else if( !fossil_isalnum(c) ){ + iVal += 2; + } + if( iVal>iBestVal ){ + iBestVal = iVal; + iBest = i; + } + } + nPrefix = iBest; + nSuffix = nShort - nPrefix; + nCommon = nPrefix + nSuffix; + } + + /* A single chunk of text inserted */ + if( nCommon==nLeft ){ + p->n = 1; + p->a[0].iStart1 = nPrefix; + p->a[0].iLen1 = 0; + p->a[0].iStart2 = nPrefix; + p->a[0].iLen2 = nRight - nCommon; + improveReadability(zLeft, zRight, p); + return; + } + + /* A single chunk of text deleted */ + if( nCommon==nRight ){ + p->n = 1; + p->a[0].iStart1 = nPrefix; + p->a[0].iLen1 = nLeft - nCommon; + p->a[0].iStart2 = nPrefix; + p->a[0].iLen2 = 0; + improveReadability(zLeft, zRight, p); + return; + } + + /* At this point we know that there is a chunk of text that has + ** changed between the left and the right. Check to see if there + ** is a large unchanged section in the middle of that changed block. + */ + nLeftDiff = nLeft - nCommon; + nRightDiff = nRight - nCommon; + if( nLeftDiff >= 4 + && nRightDiff >= 4 + && textLineChanges(&zLeft[nPrefix], nLeftDiff, + &zRight[nPrefix], nRightDiff, p)>1 + ){ + int i; + for(i=0; i<p->n; i++){ + p->a[i].iStart1 += nPrefix; + p->a[i].iStart2 += nPrefix; + } + improveReadability(zLeft, zRight, p); + return; + } + + /* If all else fails, show a single big change between left and right */ + p->n = 1; + p->a[0].iStart1 = nPrefix; + p->a[0].iLen1 = nLeft - nCommon; + p->a[0].iStart2 = nPrefix; + p->a[0].iLen2 = nRight - nCommon; + improveReadability(zLeft, zRight, p); +} + +/* +** COMMAND: test-line-diff +** Usage: %fossil% test-line-diff STRING1 STRING2 +** +** Show the differences between the two strings. Used for testing +** the oneLineChange() routine in the diff logic. +*/ +void test_line_diff(void){ + DLine a, b; + LineChange chng; + int i, j, x; + if( g.argc!=4 ) usage("STRING1 STRING2"); + a.z = g.argv[2]; + a.n = (int)strlen(a.z); + b.z = g.argv[3]; + b.n = (int)strlen(b.z); + oneLineChange(&a, &b, &chng); + fossil_print("left: [%s]\n", a.z); + for(i=x=0; i<chng.n; i++){ + int ofst = chng.a[i].iStart1; + int len = chng.a[i].iLen1; + if( len ){ + if( x==0 ){ fossil_print("%*s", 8, ""); } + while( ofst > x ){ + if( (a.z[x]&0xc0)!=0x80 ) fossil_print(" "); + x++; + } + for(j=0; j<len; j++, x++){ + if( (a.z[x]&0xc0)!=0x80 ) fossil_print("%d",i); + } + } + } + if( x ) fossil_print("\n"); + fossil_print("right: [%s]\n", b.z); + for(i=x=0; i<chng.n; i++){ + int ofst = chng.a[i].iStart2; + int len = chng.a[i].iLen2; + if( len ){ + if( x==0 ){ fossil_print("%*s", 8, ""); } + while( ofst > x ){ + if( (b.z[x]&0xc0)!=0x80 ) fossil_print(" "); + x++; + } + for(j=0; j<len; j++, x++){ + if( (b.z[x]&0xc0)!=0x80 ) fossil_print("%d",i); + } + } + } + if( x ) fossil_print("\n"); +} + +/* +** Minimum of two values +*/ +static int minInt(int a, int b){ return a<b ? a : b; } + + + +/* +** This is an abstract superclass for an object that accepts difference +** lines and formats them for display. Subclasses of this object format +** the diff output in different ways. +** +** To subclass, create an instance of the DiffBuilder object and fill +** in appropriate method implementations. +*/ +typedef struct DiffBuilder DiffBuilder; +struct DiffBuilder { + void (*xSkip)(DiffBuilder*, unsigned int, int); + void (*xCommon)(DiffBuilder*,const DLine*); + void (*xInsert)(DiffBuilder*,const DLine*); + void (*xDelete)(DiffBuilder*,const DLine*); + void (*xReplace)(DiffBuilder*,const DLine*,const DLine*); + void (*xEdit)(DiffBuilder*,const DLine*,const DLine*); + void (*xEnd)(DiffBuilder*); + unsigned int lnLeft; /* Lines seen on the left (delete) side */ + unsigned int lnRight; /* Lines seen on the right (insert) side */ + unsigned int nPending; /* Number of pending lines */ + int eState; /* State of the output */ + int width; /* Display width */ + Blob *pOut; /* Output blob */ + Blob aCol[5]; /* Holding blobs */ + DiffConfig *pCfg; /* Configuration information */ +}; + +/************************* DiffBuilderDebug ********************************/ +/* This version of DiffBuilder is used for debugging the diff and diff +** diff formatter logic. It is accessed using the (undocumented) --debug +** option to the diff command. The output is human-readable text that +** describes the various method calls that are invoked against the DiffBuilder +** object. +*/ +static void dfdebugSkip(DiffBuilder *p, unsigned int n, int isFinal){ + blob_appendf(p->pOut, "SKIP %d (%d..%d left and %d..%d right)%s\n", + n, p->lnLeft+1, p->lnLeft+n, p->lnRight+1, p->lnRight+n, + isFinal ? " FINAL" : ""); + p->lnLeft += n; + p->lnRight += n; +} +static void dfdebugCommon(DiffBuilder *p, const DLine *pLine){ + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut, "COMMON %8u %8u %.*s\n", + p->lnLeft, p->lnRight, (int)pLine->n, pLine->z); +} +static void dfdebugInsert(DiffBuilder *p, const DLine *pLine){ + p->lnRight++; + blob_appendf(p->pOut, "INSERT %8d %.*s\n", + p->lnRight, (int)pLine->n, pLine->z); +} +static void dfdebugDelete(DiffBuilder *p, const DLine *pLine){ + p->lnLeft++; + blob_appendf(p->pOut, "DELETE %8u %.*s\n", + p->lnLeft, (int)pLine->n, pLine->z); +} +static void dfdebugReplace(DiffBuilder *p, const DLine *pX, const DLine *pY){ + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut, "REPLACE %8u %.*s\n", + p->lnLeft, (int)pX->n, pX->z); + blob_appendf(p->pOut, " %8u %.*s\n", + p->lnRight, (int)pY->n, pY->z); +} +static void dfdebugEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + int i, j; + int x; + LineChange chng; + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut, "EDIT %8u %.*s\n", + p->lnLeft, (int)pX->n, pX->z); + oneLineChange(pX, pY, &chng); + for(i=x=0; i<chng.n; i++){ + int ofst = chng.a[i].iStart1; + int len = chng.a[i].iLen1; + if( len ){ + char c = '0' + i; + if( x==0 ){ blob_appendf(p->pOut, "%*s", 26, ""); } + while( ofst > x ){ + if( (pX->z[x]&0xc0)!=0x80 ) blob_append_char(p->pOut, ' '); + x++; + } + for(j=0; j<len; j++, x++){ + if( (pX->z[x]&0xc0)!=0x80 ) blob_append_char(p->pOut, c); + } + } + } + if( x ) blob_append_char(p->pOut, '\n'); + blob_appendf(p->pOut, " %8u %.*s\n", + p->lnRight, (int)pY->n, pY->z); + for(i=x=0; i<chng.n; i++){ + int ofst = chng.a[i].iStart2; + int len = chng.a[i].iLen2; + if( len ){ + char c = '0' + i; + if( x==0 ){ blob_appendf(p->pOut, "%*s", 26, ""); } + while( ofst > x ){ + if( (pY->z[x]&0xc0)!=0x80 ) blob_append_char(p->pOut, ' '); + x++; + } + for(j=0; j<len; j++, x++){ + if( (pY->z[x]&0xc0)!=0x80 ) blob_append_char(p->pOut, c); + } + } + } + if( x ) blob_append_char(p->pOut, '\n'); +} +static void dfdebugEnd(DiffBuilder *p){ + blob_appendf(p->pOut, "END with %u lines left and %u lines right\n", + p->lnLeft, p->lnRight); + fossil_free(p); +} +static DiffBuilder *dfdebugNew(Blob *pOut){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dfdebugSkip; + p->xCommon = dfdebugCommon; + p->xInsert = dfdebugInsert; + p->xDelete = dfdebugDelete; + p->xReplace = dfdebugReplace; + p->xEdit = dfdebugEdit; + p->xEnd = dfdebugEnd; + p->lnLeft = p->lnRight = 0; + p->pOut = pOut; + return p; +} + +/************************* DiffBuilderTcl ********************************/ +/* +** This formatter outputs a description of the diff formatted as TCL, for +** use by the --tk option to "diff". See also the "diff.tcl" file. The +** output can be viewed directly using the --tcl option. +** +** There is one line per method call: +** +** SKIP n -- Skip "n" lines of input +** COM string -- "string" is an unchanged context line +** INS string -- "string" is in the right file only +** DEL string -- "string" is in the left file only +** EDIT string .... -- Complex edit between left and right +** +** The EDIT verb will be followed by 3*N or 3*N+1 strings. The triples +** each show: +** +** 1. Common text +** 2. Text from the left side +** 3. Text on the right that replaces (2) from the left +** +** For inserted text (2) will be an empty string. For deleted text, (3) +** will be an empty string. (1) might be empty for the first triple if +** the line begins with an edit. After all triples, there might be one +** additional string which is a common suffix. +*/ +static void dftclSkip(DiffBuilder *p, unsigned int n, int isFinal){ + blob_appendf(p->pOut, "SKIP %u\n", n); +} +static void dftclCommon(DiffBuilder *p, const DLine *pLine){ + blob_appendf(p->pOut, "COM "); + blob_append_tcl_literal(p->pOut, pLine->z, pLine->n); + blob_append_char(p->pOut, '\n'); +} +static void dftclInsert(DiffBuilder *p, const DLine *pLine){ + blob_append(p->pOut, "INS ", -1); + blob_append_tcl_literal(p->pOut, pLine->z, pLine->n); + blob_append_char(p->pOut, '\n'); +} +static void dftclDelete(DiffBuilder *p, const DLine *pLine){ + blob_append(p->pOut, "DEL ", -1); + blob_append_tcl_literal(p->pOut, pLine->z, pLine->n); + blob_append_char(p->pOut, '\n'); +} +static void dftclReplace(DiffBuilder *p, const DLine *pX, const DLine *pY){ + blob_append(p->pOut, "EDIT \"\" ", -1); + blob_append_tcl_literal(p->pOut, pX->z, pX->n); + blob_append_char(p->pOut, ' '); + blob_append_tcl_literal(p->pOut, pY->z, pY->n); + blob_append_char(p->pOut, '\n'); +} +static void dftclEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + int i, x; + LineChange chng; + blob_append(p->pOut, "EDIT", 4); + oneLineChange(pX, pY, &chng); + for(i=x=0; i<chng.n; i++){ + blob_append_char(p->pOut, ' '); + blob_append_tcl_literal(p->pOut, pX->z + x, chng.a[i].iStart1 - x); + x = chng.a[i].iStart1; + blob_append_char(p->pOut, ' '); + blob_append_tcl_literal(p->pOut, pX->z + x, chng.a[i].iLen1); + x += chng.a[i].iLen1; + blob_append_char(p->pOut, ' '); + blob_append_tcl_literal(p->pOut, + pY->z + chng.a[i].iStart2, chng.a[i].iLen2); + } + if( x<pX->n ){ + blob_append_char(p->pOut, ' '); + blob_append_tcl_literal(p->pOut, pX->z + x, pX->n - x); + } + blob_append_char(p->pOut, '\n'); +} +static void dftclEnd(DiffBuilder *p){ + fossil_free(p); +} +static DiffBuilder *dftclNew(Blob *pOut){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dftclSkip; + p->xCommon = dftclCommon; + p->xInsert = dftclInsert; + p->xDelete = dftclDelete; + p->xReplace = dftclReplace; + p->xEdit = dftclEdit; + p->xEnd = dftclEnd; + p->pOut = pOut; + return p; +} + +/************************* DiffBuilderJson ********************************/ +/* +** This formatter generates a JSON array that describes the difference. +** +** The Json array consists of integer opcodes with each opcode followed +** by zero or more arguments: +** +** Syntax Mnemonic Description +** ----------- -------- -------------------------- +** 0 END This is the end of the diff +** 1 INTEGER SKIP Skip N lines from both files +** 2 STRING COMMON The line shown by STRING is in both files +** 3 STRING INSERT The line STRING is in only the right file +** 4 STRING DELETE The STRING line is in only the left file +** 5 SUBARRAY EDIT One line is different on left and right. +** +** The SUBARRAY is an array of 3*N+1 strings with N>=0. The triples +** represent common-text, left-text, and right-text. The last string +** in SUBARRAY is the common-suffix. Any string can be empty if it does +** not apply. +*/ +static void dfjsonSkip(DiffBuilder *p, unsigned int n, int isFinal){ + blob_appendf(p->pOut, "1,%u,\n", n); +} +static void dfjsonCommon(DiffBuilder *p, const DLine *pLine){ + blob_append(p->pOut, "2,",2); + blob_append_json_literal(p->pOut, pLine->z, (int)pLine->n); + blob_append(p->pOut, ",\n",2); +} +static void dfjsonInsert(DiffBuilder *p, const DLine *pLine){ + blob_append(p->pOut, "3,",2); + blob_append_json_literal(p->pOut, pLine->z, (int)pLine->n); + blob_append(p->pOut, ",\n",2); +} +static void dfjsonDelete(DiffBuilder *p, const DLine *pLine){ + blob_append(p->pOut, "4,",2); + blob_append_json_literal(p->pOut, pLine->z, (int)pLine->n); + blob_append(p->pOut, ",\n",2); +} +static void dfjsonReplace(DiffBuilder *p, const DLine *pX, const DLine *pY){ + blob_append(p->pOut, "5,[\"\",",-1); + blob_append_json_literal(p->pOut, pX->z, (int)pX->n); + blob_append(p->pOut, ",",1); + blob_append_json_literal(p->pOut, pY->z, (int)pY->n); + blob_append(p->pOut, ",\"\"],\n",-1); +} +static void dfjsonEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + int i, x; + LineChange chng; + blob_append(p->pOut, "5,[", 3); + oneLineChange(pX, pY, &chng); + for(i=x=0; i<chng.n; i++){ + if(i>0){ + blob_append_char(p->pOut, ','); + } + blob_append_json_literal(p->pOut, pX->z + x, chng.a[i].iStart1 - x); + x = chng.a[i].iStart1; + blob_append_char(p->pOut, ','); + blob_append_json_literal(p->pOut, pX->z + x, chng.a[i].iLen1); + x += chng.a[i].iLen1; + blob_append_char(p->pOut, ','); + blob_append_json_literal(p->pOut, + pY->z + chng.a[i].iStart2, chng.a[i].iLen2); + } + blob_append_char(p->pOut, ','); + blob_append_json_literal(p->pOut, pX->z + x, pX->n - x); + blob_append(p->pOut, "],\n",3); +} +static void dfjsonEnd(DiffBuilder *p){ + blob_append(p->pOut, "0]}", 3); + fossil_free(p); +} +static DiffBuilder *dfjsonNew(Blob *pOut){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dfjsonSkip; + p->xCommon = dfjsonCommon; + p->xInsert = dfjsonInsert; + p->xDelete = dfjsonDelete; + p->xReplace = dfjsonReplace; + p->xEdit = dfjsonEdit; + p->xEnd = dfjsonEnd; + p->lnLeft = p->lnRight = 0; + p->pOut = pOut; + blob_append_char(pOut, '['); + return p; +} + +/************************* DiffBuilderUnified********************************/ +/* This formatter generates a unified diff for HTML. +** +** The result is a <table> with four columns. The four columns hold: +** +** 1. The line numbers for the first file. +** 2. The line numbers for the second file. +** 3. The "diff mark": "+" or "-" or just a space +** 4. Text of the line +** +** Inserted lines are marked with <ins> and deleted lines are marked +** with <del>. The whole line is marked this way, not just the part that +** changed. The part that change has an additional nested <ins> or <del>. +** The CSS needs to be set up such that a single <ins> or <del> gives a +** light background and a nested <ins> or <del> gives a darker background. +** Additional attributes (like bold font) might also be added to nested +** <ins> and <del> since those are the characters that have actually +** changed. +** +** Accumulator strategy: +** +** * Delete line numbers are output directly to p->pOut +** * Insert line numbers accumulate in p->aCol[0]. +** * Separator marks accumulate in p->aCol[1]. +** * Change text accumulates in p->aCol[2]. +** * Pending insert line numbers go into p->aCol[3]. +** * Pending insert text goes into p->aCol[4]. +** +** eState is 1 if text has an open <del> +*/ +static void dfunifiedFinishDelete(DiffBuilder *p){ + if( p->eState==0 ) return; + blob_append(p->pOut, "</del>", 6); + blob_append(&p->aCol[2], "</del>", 6); + p->eState = 0; +} +static void dfunifiedFinishInsert(DiffBuilder *p){ + unsigned int i; + if( p->nPending==0 ) return; + dfunifiedFinishDelete(p); + + /* Blank lines for delete line numbers for each inserted line */ + for(i=0; i<p->nPending; i++) blob_append_char(p->pOut, '\n'); + + /* Insert line numbers */ + blob_append(&p->aCol[0], "<ins>", 5); + blob_append_xfer(&p->aCol[0], &p->aCol[3]); + blob_append(&p->aCol[0], "</ins>", 6); + + /* "+" marks for the separator on inserted lines */ + for(i=0; i<p->nPending; i++) blob_append(&p->aCol[1], "+\n", 2); + + /* Text of the inserted lines */ + blob_append(&p->aCol[2], "<ins>", 5); + blob_append_xfer(&p->aCol[2], &p->aCol[4]); + blob_append(&p->aCol[2], "</ins>", 6); + + p->nPending = 0; +} +static void dfunifiedFinishRow(DiffBuilder *p){ + dfunifiedFinishDelete(p); + dfunifiedFinishInsert(p); + if( blob_size(&p->aCol[0])==0 ) return; + blob_append(p->pOut, "</pre></td><td class=\"diffln difflnr\"><pre>\n", -1); + blob_append_xfer(p->pOut, &p->aCol[0]); + blob_append(p->pOut, "</pre></td><td class=\"diffsep\"><pre>\n", -1); + blob_append_xfer(p->pOut, &p->aCol[1]); + blob_append(p->pOut, "</pre></td><td class=\"difftxt difftxtu\"><pre>\n",-1); + blob_append_xfer(p->pOut, &p->aCol[2]); + blob_append(p->pOut, "</pre></td></tr>\n", -1); +} +static void dfunifiedStartRow(DiffBuilder *p){ + if( blob_size(&p->aCol[0])>0 ) return; + blob_appendf(p->pOut,"<tr id=\"chunk%d\" class=\"diffchunk\">" + "<td class=\"diffln difflnl\"><pre>\n", ++nChunk); +} +static void dfunifiedSkip(DiffBuilder *p, unsigned int n, int isFinal){ + dfunifiedFinishRow(p); + if( p->pCfg && p->pCfg->zLeftHash ){ + blob_appendf(p->pOut, + "<tr class=\"diffskip\" data-startln=\"%d\" data-endln=\"%d\"" + " id=\"skip%xh%xi%x\">\n", + p->lnLeft+1, p->lnLeft+n, + nChunk, p->lnLeft, n); + }else{ + blob_append(p->pOut, "<tr>", 4); + } + blob_append(p->pOut, "<td class=\"diffln difflne\">" + "︙</td><td></td><td></td></tr>\n", -1); + p->lnLeft += n; + p->lnRight += n; +} +static void dfunifiedCommon(DiffBuilder *p, const DLine *pLine){ + dfunifiedStartRow(p); + dfunifiedFinishDelete(p); + dfunifiedFinishInsert(p); + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + blob_appendf(&p->aCol[0],"%d\n",p->lnRight); + blob_append_char(&p->aCol[1], '\n'); + htmlize_to_blob(&p->aCol[2], pLine->z, (int)pLine->n); + blob_append_char(&p->aCol[2], '\n'); +} +static void dfunifiedInsert(DiffBuilder *p, const DLine *pLine){ + dfunifiedStartRow(p); + p->lnRight++; + blob_appendf(&p->aCol[3],"%d\n", p->lnRight); + blob_append(&p->aCol[4], "<ins>", 5); + htmlize_to_blob(&p->aCol[4], pLine->z, (int)pLine->n); + blob_append(&p->aCol[4], "</ins>\n", 7); + p->nPending++; +} +static void dfunifiedDelete(DiffBuilder *p, const DLine *pLine){ + dfunifiedStartRow(p); + dfunifiedFinishInsert(p); + if( p->eState==0 ){ + dfunifiedFinishInsert(p); + blob_append(p->pOut, "<del>", 5); + blob_append(&p->aCol[2], "<del>", 5); + p->eState = 1; + } + p->lnLeft++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + blob_append_char(&p->aCol[0],'\n'); + blob_append(&p->aCol[1],"-\n",2); + blob_append(&p->aCol[2], "<del>", 5); + htmlize_to_blob(&p->aCol[2], pLine->z, (int)pLine->n); + blob_append(&p->aCol[2], "</del>\n", 7); +} +static void dfunifiedReplace(DiffBuilder *p, const DLine *pX, const DLine *pY){ + dfunifiedStartRow(p); + if( p->eState==0 ){ + dfunifiedFinishInsert(p); + blob_append(p->pOut, "<del>", 5); + blob_append(&p->aCol[2], "<del>", 5); + p->eState = 1; + } + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + blob_append_char(&p->aCol[0], '\n'); + blob_append(&p->aCol[1], "-\n", 2); + + htmlize_to_blob(&p->aCol[2], pX->z, pX->n); + blob_append_char(&p->aCol[2], '\n'); + + blob_appendf(&p->aCol[3],"%d\n", p->lnRight); + + htmlize_to_blob(&p->aCol[4], pY->z, pY->n); + blob_append_char(&p->aCol[4], '\n'); + p->nPending++; +} +static void dfunifiedEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + int i; + int x; + LineChange chng; + oneLineChange(pX, pY, &chng); + dfunifiedStartRow(p); + if( p->eState==0 ){ + dfunifiedFinishInsert(p); + blob_append(p->pOut, "<del>", 5); + blob_append(&p->aCol[2], "<del>", 5); + p->eState = 1; + } + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + blob_append_char(&p->aCol[0], '\n'); + blob_append(&p->aCol[1], "-\n", 2); + + for(i=x=0; i<chng.n; i++){ + int ofst = chng.a[i].iStart1; + int len = chng.a[i].iLen1; + if( len ){ + htmlize_to_blob(&p->aCol[2], pX->z+x, ofst - x); + x = ofst; + blob_append(&p->aCol[2], "<del>", 5); + htmlize_to_blob(&p->aCol[2], pX->z+x, len); + x += len; + blob_append(&p->aCol[2], "</del>", 6); + } + } + htmlize_to_blob(&p->aCol[2], pX->z+x, pX->n - x); + blob_append_char(&p->aCol[2], '\n'); + + blob_appendf(&p->aCol[3],"%d\n", p->lnRight); + for(i=x=0; i<chng.n; i++){ + int ofst = chng.a[i].iStart2; + int len = chng.a[i].iLen2; + if( len ){ + htmlize_to_blob(&p->aCol[4], pY->z+x, ofst - x); + x = ofst; + blob_append(&p->aCol[4], "<ins>", 5); + htmlize_to_blob(&p->aCol[4], pY->z+x, len); + x += len; + blob_append(&p->aCol[4], "</ins>", 6); + } + } + htmlize_to_blob(&p->aCol[4], pY->z+x, pY->n - x); + blob_append_char(&p->aCol[4], '\n'); + p->nPending++; +} +static void dfunifiedEnd(DiffBuilder *p){ + dfunifiedFinishRow(p); + blob_append(p->pOut, "</table>\n",-1); + fossil_free(p); +} +static DiffBuilder *dfunifiedNew(Blob *pOut, DiffConfig *pCfg){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dfunifiedSkip; + p->xCommon = dfunifiedCommon; + p->xInsert = dfunifiedInsert; + p->xDelete = dfunifiedDelete; + p->xReplace = dfunifiedReplace; + p->xEdit = dfunifiedEdit; + p->xEnd = dfunifiedEnd; + p->lnLeft = p->lnRight = 0; + p->eState = 0; + p->nPending = 0; + p->pOut = pOut; + if( pCfg->zLeftHash ){ + blob_appendf(pOut, "<table class=\"diff udiff\" data-lefthash=\"%s\">\n", + pCfg->zLeftHash); + }else{ + blob_append(pOut, "<table class=\"diff udiff\">\n", -1); + } + blob_init(&p->aCol[0], 0, 0); + blob_init(&p->aCol[1], 0, 0); + blob_init(&p->aCol[2], 0, 0); + blob_init(&p->aCol[3], 0, 0); + blob_init(&p->aCol[4], 0, 0); + p->pCfg = pCfg; + return p; +} + +/************************* DiffBuilderSplit ******************************/ +/* This formatter creates a side-by-side diff in HTML. The output is a +** <table> with 5 columns: +** +** 1. Line numbers for the first file. +** 2. Text for the first file. +** 3. The difference mark. "<", ">", "|" or blank +** 4. Line numbers for the second file. +** 5. Text for the second file. +** +** The <ins> and <del> strategy is the same as for unified diff above. +** In fact, the same CSS can be used for both. +** +** Accumulator strategy: +** +** * Left line numbers are output directly to p->pOut +** * Left text accumulates in p->aCol[0]. +** * Edit marks accumulates in p->aCol[1]. +** * Right line numbers accumulate in p->aCol[2]. +** * Right text accumulates in p->aCol[3]. +** +** eState: +** 0 In common block +** 1 Have <del> on the left +** 2 Have <ins> on the right +** 3 Have <del> on left and <ins> on the right +*/ +static void dfsplitChangeState(DiffBuilder *p, int newState){ + if( p->eState == newState ) return; + if( (p->eState&1)==0 && (newState & 1)!=0 ){ + blob_append(p->pOut, "<del>", 5); + blob_append(&p->aCol[0], "<del>", 5); + p->eState |= 1; + }else if( (p->eState&1)!=0 && (newState & 1)==0 ){ + blob_append(p->pOut, "</del>", 6); + blob_append(&p->aCol[0], "</del>", 6); + p->eState &= ~1; + } + if( (p->eState&2)==0 && (newState & 2)!=0 ){ + blob_append(&p->aCol[2], "<ins>", 5); + blob_append(&p->aCol[3], "<ins>", 5); + p->eState |= 2; + }else if( (p->eState&2)!=0 && (newState & 2)==0 ){ + blob_append(&p->aCol[2], "</ins>", 6); + blob_append(&p->aCol[3], "</ins>", 6); + p->eState &= ~2; + } +} +static void dfsplitFinishRow(DiffBuilder *p){ + if( blob_size(&p->aCol[0])==0 ) return; + dfsplitChangeState(p, 0); + blob_append(p->pOut, "</pre></td><td class=\"difftxt difftxtl\"><pre>\n",-1); + blob_append_xfer(p->pOut, &p->aCol[0]); + blob_append(p->pOut, "</pre></td><td class=\"diffsep\"><pre>\n", -1); + blob_append_xfer(p->pOut, &p->aCol[1]); + blob_append(p->pOut, "</pre></td><td class=\"diffln difflnr\"><pre>\n",-1); + blob_append_xfer(p->pOut, &p->aCol[2]); + blob_append(p->pOut, "</pre></td><td class=\"difftxt difftxtr\"><pre>\n",-1); + blob_append_xfer(p->pOut, &p->aCol[3]); + blob_append(p->pOut, "</pre></td></tr>\n", -1); +} +static void dfsplitStartRow(DiffBuilder *p){ + if( blob_size(&p->aCol[0])>0 ) return; + blob_appendf(p->pOut,"<tr id=\"chunk%d\" class=\"diffchunk\">" + "<td class=\"diffln difflnl\"><pre>\n", ++nChunk); + p->eState = 0; +} +static void dfsplitSkip(DiffBuilder *p, unsigned int n, int isFinal){ + dfsplitFinishRow(p); + if( p->pCfg && p->pCfg->zLeftHash ){ + blob_appendf(p->pOut, + "<tr class=\"diffskip\" data-startln=\"%d\" data-endln=\"%d\"" + " id=\"skip%xh%xi%x\">\n", + p->lnLeft+1, p->lnLeft+n, + nChunk,p->lnLeft,n); + }else{ + blob_append(p->pOut, "<tr>", 4); + } + blob_append(p->pOut, + "<td class=\"diffln difflnl difflne\">︙</td>" + "<td></td><td></td>" + "<td class=\"diffln difflnr difflne\">︙</td>" + "<td/td></tr>\n", -1); + p->lnLeft += n; + p->lnRight += n; +} +static void dfsplitCommon(DiffBuilder *p, const DLine *pLine){ + dfsplitStartRow(p); + dfsplitChangeState(p, 0); + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + htmlize_to_blob(&p->aCol[0], pLine->z, (int)pLine->n); + blob_append_char(&p->aCol[0], '\n'); + blob_append_char(&p->aCol[1], '\n'); + blob_appendf(&p->aCol[2],"%d\n",p->lnRight); + htmlize_to_blob(&p->aCol[3], pLine->z, (int)pLine->n); + blob_append_char(&p->aCol[3], '\n'); +} +static void dfsplitInsert(DiffBuilder *p, const DLine *pLine){ + dfsplitStartRow(p); + dfsplitChangeState(p, 2); + p->lnRight++; + blob_append_char(p->pOut, '\n'); + blob_append_char(&p->aCol[0], '\n'); + blob_append(&p->aCol[1], ">\n", -1); + blob_appendf(&p->aCol[2],"%d\n", p->lnRight); + blob_append(&p->aCol[3], "<ins>", 5); + htmlize_to_blob(&p->aCol[3], pLine->z, (int)pLine->n); + blob_append(&p->aCol[3], "</ins>\n", 7); +} +static void dfsplitDelete(DiffBuilder *p, const DLine *pLine){ + dfsplitStartRow(p); + dfsplitChangeState(p, 1); + p->lnLeft++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + blob_append(&p->aCol[0], "<del>", 5); + htmlize_to_blob(&p->aCol[0], pLine->z, (int)pLine->n); + blob_append(&p->aCol[0], "</del>\n", 7); + blob_append(&p->aCol[1], "<\n", -1); + blob_append_char(&p->aCol[2],'\n'); + blob_append_char(&p->aCol[3],'\n'); +} +static void dfsplitReplace(DiffBuilder *p, const DLine *pX, const DLine *pY){ + dfsplitStartRow(p); + dfsplitChangeState(p, 3); + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + htmlize_to_blob(&p->aCol[0], pX->z, pX->n); + blob_append_char(&p->aCol[0], '\n'); + + blob_append(&p->aCol[1], "|\n", 2); + + blob_appendf(&p->aCol[2],"%d\n", p->lnRight); + + htmlize_to_blob(&p->aCol[3], pY->z, pY->n); + blob_append_char(&p->aCol[3], '\n'); +} +static void dfsplitEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + int i; + int x; + LineChange chng; + oneLineChange(pX, pY, &chng); + dfsplitStartRow(p); + dfsplitChangeState(p, 3); + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%d\n", p->lnLeft); + for(i=x=0; i<chng.n; i++){ + int ofst = chng.a[i].iStart1; + int len = chng.a[i].iLen1; + if( len ){ + htmlize_to_blob(&p->aCol[0], pX->z+x, ofst - x); + x = ofst; + if( chng.a[i].iLen2 ){ + blob_append(&p->aCol[0], "<del class='edit'>", -1); + }else{ + blob_append(&p->aCol[0], "<del>", 5); + } + htmlize_to_blob(&p->aCol[0], pX->z+x, len); + x += len; + blob_append(&p->aCol[0], "</del>", 6); + } + } + htmlize_to_blob(&p->aCol[0], pX->z+x, pX->n - x); + blob_append_char(&p->aCol[0], '\n'); + + blob_append(&p->aCol[1], "|\n", 2); + + blob_appendf(&p->aCol[2],"%d\n", p->lnRight); + for(i=x=0; i<chng.n; i++){ + int ofst = chng.a[i].iStart2; + int len = chng.a[i].iLen2; + if( len ){ + htmlize_to_blob(&p->aCol[3], pY->z+x, ofst - x); + x = ofst; + if( chng.a[i].iLen1 ){ + blob_append(&p->aCol[3], "<ins class='edit'>", -1); + }else{ + blob_append(&p->aCol[3], "<ins>", 5); + } + htmlize_to_blob(&p->aCol[3], pY->z+x, len); + x += len; + blob_append(&p->aCol[3], "</ins>", 6); + } + } + htmlize_to_blob(&p->aCol[3], pY->z+x, pY->n - x); + blob_append_char(&p->aCol[3], '\n'); +} +static void dfsplitEnd(DiffBuilder *p){ + dfsplitFinishRow(p); + blob_append(p->pOut, "</table>\n",-1); + fossil_free(p); +} +static DiffBuilder *dfsplitNew(Blob *pOut, DiffConfig *pCfg){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dfsplitSkip; + p->xCommon = dfsplitCommon; + p->xInsert = dfsplitInsert; + p->xDelete = dfsplitDelete; + p->xReplace = dfsplitReplace; + p->xEdit = dfsplitEdit; + p->xEnd = dfsplitEnd; + p->lnLeft = p->lnRight = 0; + p->eState = 0; + p->pOut = pOut; + if( pCfg->zLeftHash ){ + blob_appendf(pOut, + "<table class=\"diff splitdiff\" data-lefthash=\"%s\">\n", + pCfg->zLeftHash); + }else{ + blob_append(pOut, "<table class=\"diff splitdiff\">\n", -1); + } + blob_init(&p->aCol[0], 0, 0); + blob_init(&p->aCol[1], 0, 0); + blob_init(&p->aCol[2], 0, 0); + blob_init(&p->aCol[3], 0, 0); + blob_init(&p->aCol[4], 0, 0); + p->pCfg = pCfg; + return p; +} + +/************************* DiffBuilderSbs ******************************/ +/* This formatter creates a side-by-side diff in text. +*/ +static void dfsbsSkip(DiffBuilder *p, unsigned int n, int isFinal){ + if( (p->lnLeft || p->lnRight) && !isFinal ){ + blob_appendf(p->pOut, "%.*c\n", p->width*2 + 16, '.'); + } + p->lnLeft += n; + p->lnRight += n; +} + +/* +** Append at least iMin characters (not bytes) and at most iMax characters +** from pX onto the into of p. +** +** This comment contains multibyte unicode characters (ĂĽ, Æ, ð) in order +** to test the ability of the diff code to handle such characters. +*/ +static void sbs_append_chars(Blob *p, int iMin, int iMax, const DLine *pX){ + int i; + const char *z = pX->z; + for(i=0; i<iMax && i<pX->n; i++){ + char c = z[i]; + blob_append_char(p, c); + if( (c&0xc0)==0x80 ){ iMin++; iMax++; } + } + while( i<iMin ){ + blob_append_char(p, ' '); + i++; + } +} + +static void dfsbsCommon(DiffBuilder *p, const DLine *pLine){ + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%6u ", p->lnLeft); + sbs_append_chars(p->pOut, p->width, p->width, pLine); + blob_appendf(p->pOut," %6u ", p->lnRight); + sbs_append_chars(p->pOut, 0, p->width, pLine); + blob_append_char(p->pOut, '\n'); +} +static void dfsbsInsert(DiffBuilder *p, const DLine *pLine){ + p->lnRight++; + blob_appendf(p->pOut,"%6s %*s > %6u ", + "", p->width, "", p->lnRight); + sbs_append_chars(p->pOut, 0, p->width, pLine); + blob_append_char(p->pOut, '\n'); +} +static void dfsbsDelete(DiffBuilder *p, const DLine *pLine){ + p->lnLeft++; + blob_appendf(p->pOut,"%6u ", p->lnLeft); + sbs_append_chars(p->pOut, p->width, p->width, pLine); + blob_append(p->pOut," <\n", 3); +} +static void dfsbsEdit(DiffBuilder *p, const DLine *pX, const DLine *pY){ + p->lnLeft++; + p->lnRight++; + blob_appendf(p->pOut,"%6u ", p->lnLeft); + sbs_append_chars(p->pOut, p->width, p->width, pX); + blob_appendf(p->pOut, " | %6u ", p->lnRight); + sbs_append_chars(p->pOut, 0, p->width, pY); + blob_append_char(p->pOut, '\n'); +} +static void dfsbsEnd(DiffBuilder *p){ + fossil_free(p); +} +static DiffBuilder *dfsbsNew(Blob *pOut, DiffConfig *pCfg){ + DiffBuilder *p = fossil_malloc(sizeof(*p)); + p->xSkip = dfsbsSkip; + p->xCommon = dfsbsCommon; + p->xInsert = dfsbsInsert; + p->xDelete = dfsbsDelete; + p->xReplace = dfsbsEdit; + p->xEdit = dfsbsEdit; + p->xEnd = dfsbsEnd; + p->lnLeft = p->lnRight = 0; + p->width = diff_width(pCfg); + p->pOut = pOut; + return p; +} +/****************************************************************************/ +/* +** Return the number between 0 and 100 that is smaller the closer pA and +** pB match. Return 0 for a perfect match. Return 100 if pA and pB are +** completely different. +** +** The current algorithm is as follows: +** +** (1) Remove leading and trailing whitespace. +** (2) Truncate both strings to at most 250 characters +** (3) If the two strings have a common prefix, measure that prefix +** (4) Find the length of the longest common subsequence that is +** at least 150% longer than the common prefix. +** (5) Longer common subsequences yield lower scores. +*/ +static int match_dline(DLine *pA, DLine *pB){ + const char *zA; /* Left string */ + const char *zB; /* right string */ + int nA; /* Bytes in zA[] */ + int nB; /* Bytes in zB[] */ + int nMin; + int nPrefix; + int avg; /* Average length of A and B */ + int i, j, k; /* Loop counters */ + int best = 0; /* Longest match found so far */ + int score; /* Final score. 0..100 */ + unsigned char c; /* Character being examined */ + unsigned char aFirst[256]; /* aFirst[X] = index in zB[] of first char X */ + unsigned char aNext[252]; /* aNext[i] = index in zB[] of next zB[i] char */ + + zA = pA->z; + if( pA->nw==0 && pA->n ){ + for(i=0; i<pA->n && diff_isspace(zA[i]); i++){} + pA->indent = i; + for(j=pA->n-1; j>i && diff_isspace(zA[j]); j--){} + pA->nw = j - i + 1; + } + zA += pA->indent; + nA = pA->nw; + + zB = pB->z; + if( pB->nw==0 && pB->n ){ + for(i=0; i<pB->n && diff_isspace(zB[i]); i++){} + pB->indent = i; + for(j=pB->n-1; j>i && diff_isspace(zB[j]); j--){} + pB->nw = j - i + 1; + } + zB += pB->indent; + nB = pB->nw; + + if( nA>250 ) nA = 250; + if( nB>250 ) nB = 250; + avg = (nA+nB)/2; + if( avg==0 ) return 0; + nMin = nA; + if( nB<nMin ) nMin = nB; + if( nMin==0 ) return 68; + for(nPrefix=0; nPrefix<nMin && zA[nPrefix]==zB[nPrefix]; nPrefix++){} + best = 0; + if( nPrefix>5 && nPrefix>nMin/2 ){ + best = nPrefix*3/2; + if( best>=avg - 2 ) best = avg - 2; + } + if( nA==nB && memcmp(zA, zB, nA)==0 ) return 0; + memset(aFirst, 0xff, sizeof(aFirst)); + zA--; zB--; /* Make both zA[] and zB[] 1-indexed */ + for(i=nB; i>0; i--){ + c = (unsigned char)zB[i]; + aNext[i] = aFirst[c]; + aFirst[c] = i; + } + for(i=1; i<=nA-best; i++){ + c = (unsigned char)zA[i]; + for(j=aFirst[c]; j<nB-best && memcmp(&zA[i],&zB[j],best)==0; j = aNext[j]){ + int limit = minInt(nA-i, nB-j); + for(k=best; k<=limit && zA[k+i]==zB[k+j]; k++){} + if( k>best ) best = k; + } + } + score = 5 + ((best>=avg) ? 0 : (avg - best)*95/avg); + +#if 0 + fprintf(stderr, "A: [%.*s]\nB: [%.*s]\nbest=%d avg=%d score=%d\n", + nA, zA+1, nB, zB+1, best, avg, score); +#endif + + /* Return the result */ + return score; +} + +/* +** COMMAND: test-line-match +** Usage: %fossil test-line-match STRING1 STRING2 +** +** Return a score from 0 to 100 that is how similar STRING1 is to +** STRING2. Smaller numbers mean more similar. 0 is an exact match. +** +** This command is used to test to match_dline() function in the +** internal Fossil diff logic. +*/ +void test_dline_match(void){ + DLine a, b; + int x; + if( g.argc!=4 ) usage("STRING1 STRING2"); + a.z = g.argv[2]; + a.n = (int)strlen(a.z); + b.z = g.argv[3]; + b.n = (int)strlen(b.z); + x = match_dline(&a, &b); + fossil_print("%d\n", x); +} + +/* Forward declarations for recursion */ +static unsigned char *diffBlockAlignment( + DLine *aLeft, int nLeft, /* Text on the left */ + DLine *aRight, int nRight, /* Text on the right */ + DiffConfig *pCfg, /* Configuration options */ + int *pNResult /* OUTPUT: Bytes of result */ +); +static void longestCommonSequence( + DContext *p, /* Two files being compared */ + int iS1, int iE1, /* Range of lines in p->aFrom[] */ + int iS2, int iE2, /* Range of lines in p->aTo[] */ + int *piSX, int *piEX, /* Write p->aFrom[] common segment here */ + int *piSY, int *piEY /* Write p->aTo[] common segment here */ +); + +/* +** Make a copy of a list of nLine DLine objects from one array to +** another. Hash the new array to ignore whitespace. +*/ +static void diffDLineXfer( + DLine *aTo, + const DLine *aFrom, + int nLine +){ + int i, j, k; + u64 h, h2; + for(i=0; i<nLine; i++) aTo[i].iHash = 0; + for(i=0; i<nLine; i++){ + const char *z = aFrom[i].z; + int n = aFrom[i].n; + for(j=0; j<n && diff_isspace(z[j]); j++){} + aTo[i].z = &z[j]; + for(k=aFrom[i].n; k>j && diff_isspace(z[k-1]); k--){} + aTo[i].n = n = k-j; + aTo[i].indent = 0; + aTo[i].nw = 0; + for(h=0; j<k; j++){ + char c = z[j]; + if( !diff_isspace(c) ){ + h = (h^c)*9000000000000000041LL; + } + } + aTo[i].h = h = ((h%281474976710597LL)<<LENGTH_MASK_SZ) | n; + h2 = h % nLine; + aTo[i].iNext = aTo[h2].iHash; + aTo[h2].iHash = i+1; + } +} + + +/* +** For a difficult diff-block alignment that was originally for +** the default consider-all-whitespace algorithm, try to find the +** longest common subsequence between the two blocks that involves +** only whitespace changes. +*/ +static unsigned char *diffBlockAlignmentIgnoreSpace( + DLine *aLeft, int nLeft, /* Text on the left */ + DLine *aRight, int nRight, /* Text on the right */ + DiffConfig *pCfg, /* Configuration options */ + int *pNResult /* OUTPUT: Bytes of result */ +){ + DContext dc; + int iSX, iEX; /* Start and end of LCS on the left */ + int iSY, iEY; /* Start and end of the LCS on the right */ + unsigned char *a1, *a2; + int n1, n2, nLCS; + + dc.aEdit = 0; + dc.nEdit = 0; + dc.nEditAlloc = 0; + dc.nFrom = nLeft; + dc.nTo = nRight; + dc.xDiffer = compare_dline_ignore_allws; + dc.aFrom = fossil_malloc( sizeof(DLine)*(nLeft+nRight) ); + dc.aTo = &dc.aFrom[dc.nFrom]; + diffDLineXfer(dc.aFrom, aLeft, nLeft); + diffDLineXfer(dc.aTo, aRight, nRight); + longestCommonSequence(&dc,0,nLeft,0,nRight,&iSX,&iEX,&iSY,&iEY); + fossil_free(dc.aFrom); + nLCS = iEX - iSX; + if( nLCS<5 ) return 0; /* No good LCS was found */ + + if( pCfg->diffFlags & DIFF_DEBUG ){ + fossil_print(" LCS size=%d\n" + " [%.*s]\n" + " [%.*s]\n", + nLCS, aLeft[iSX].n, aLeft[iSX].z, + aLeft[iEX-1].n, aLeft[iEX-1].z); + } + + a1 = diffBlockAlignment(aLeft,iSX,aRight,iSY,pCfg,&n1); + a2 = diffBlockAlignment(aLeft+iEX, nLeft-iEX, + aRight+iEY, nRight-iEY, + pCfg, &n2); + a1 = fossil_realloc(a1, n1+nLCS+n2); + memcpy(a1+n1+nLCS,a2,n2); + memset(a1+n1,3,nLCS); + fossil_free(a2); + *pNResult = n1+n2+nLCS; + return a1; +} + + +/* +** This is a helper route for diffBlockAlignment(). In this case, +** a very large block is encountered that might be too expensive to +** use the O(N*N) Wagner edit distance algorithm. So instead, this +** block implements a less-precise but faster O(N*logN) divide-and-conquer +** approach. +*/ +static unsigned char *diffBlockAlignmentDivideAndConquer( + DLine *aLeft, int nLeft, /* Text on the left */ + DLine *aRight, int nRight, /* Text on the right */ + DiffConfig *pCfg, /* Configuration options */ + int *pNResult /* OUTPUT: Bytes of result */ +){ + DLine *aSmall; /* The smaller of aLeft and aRight */ + DLine *aBig; /* The larger of aLeft and aRight */ + int nSmall, nBig; /* Size of aSmall and aBig. nSmall<=nBig */ + int iDivSmall, iDivBig; /* Divider point for aSmall and aBig */ + int iDivLeft, iDivRight; /* Divider point for aLeft and aRight */ + unsigned char *a1, *a2; /* Results of the alignments on two halves */ + int n1, n2; /* Number of entries in a1 and a2 */ + int score, bestScore; /* Score and best score seen so far */ + int i; /* Loop counter */ + + if( nLeft>nRight ){ + aSmall = aRight; + nSmall = nRight; + aBig = aLeft; + nBig = nLeft; + }else{ + aSmall = aLeft; + nSmall = nLeft; + aBig = aRight; + nBig = nRight; + } + iDivBig = nBig/2; + iDivSmall = nSmall/2; + + if( pCfg->diffFlags & DIFF_DEBUG ){ + fossil_print(" Divide at [%.*s]\n", + aBig[iDivBig].n, aBig[iDivBig].z); + } + + bestScore = 10000; + for(i=0; i<nSmall; i++){ + score = match_dline(aBig+iDivBig, aSmall+i) + abs(i-nSmall/2)*2; + if( score<bestScore ){ + bestScore = score; + iDivSmall = i; + } + } + if( aSmall==aRight ){ + iDivRight = iDivSmall; + iDivLeft = iDivBig; + }else{ + iDivRight = iDivBig; + iDivLeft = iDivSmall; + } + a1 = diffBlockAlignment(aLeft,iDivLeft,aRight,iDivRight,pCfg,&n1); + a2 = diffBlockAlignment(aLeft+iDivLeft, nLeft-iDivLeft, + aRight+iDivRight, nRight-iDivRight, + pCfg, &n2); + a1 = fossil_realloc(a1, n1+n2 ); + memcpy(a1+n1,a2,n2); + fossil_free(a2); + *pNResult = n1+n2; + return a1; +} + + +/* +** The threshold at which diffBlockAlignment transitions from the +** O(N*N) Wagner minimum-edit-distance algorithm to a less process +** O(NlogN) divide-and-conquer approach. +*/ +#define DIFF_ALIGN_MX 1225 + +/* +** There is a change block in which nLeft lines of text on the left are +** converted into nRight lines of text on the right. This routine computes +** how the lines on the left line up with the lines on the right. +** +** The return value is a buffer of unsigned characters, obtained from +** fossil_malloc(). (The caller needs to free the return value using +** fossil_free().) Entries in the returned array have values as follows: +** +** 1. Delete the next line of pLeft. +** 2. Insert the next line of pRight. +** 3. The next line of pLeft changes into the next line of pRight. +** 4. Delete one line from pLeft and add one line to pRight. +** +** The length of the returned array will be at most nLeft+nRight bytes. +** If the first bytes is 4, that means we could not compute reasonable +** alignment between the two blocks. +** +** Algorithm: Wagner's minimum edit-distance algorithm, modified by +** adding a cost to each match based on how well the two rows match +** each other. Insertion and deletion costs are 50. Match costs +** are between 0 and 100 where 0 is a perfect match 100 is a complete +** mismatch. +*/ +static unsigned char *diffBlockAlignment( + DLine *aLeft, int nLeft, /* Text on the left */ + DLine *aRight, int nRight, /* Text on the right */ + DiffConfig *pCfg, /* Configuration options */ + int *pNResult /* OUTPUT: Bytes of result */ +){ + int i, j, k; /* Loop counters */ + int *a; /* One row of the Wagner matrix */ + int *pToFree; /* Space that needs to be freed */ + unsigned char *aM; /* Wagner result matrix */ + int aBuf[100]; /* Stack space for a[] if nRight not to big */ + + if( nLeft==0 ){ + aM = fossil_malloc( nRight + 2 ); + memset(aM, 2, nRight); + *pNResult = nRight; + return aM; + } + if( nRight==0 ){ + aM = fossil_malloc( nLeft + 2 ); + memset(aM, 1, nLeft); + *pNResult = nLeft; + return aM; + } + + if( pCfg->diffFlags & DIFF_DEBUG ){ + fossil_print("BlockAlignment:\n [%.*s] + %d\n [%.*s] + %d\n", + aLeft[0].n, aLeft[0].z, nLeft, + aRight[0].n, aRight[0].z, nRight); + } + + /* For large alignments, try to use alternative algorithms that are + ** faster than the O(N*N) Wagner edit distance. + */ + if( (i64)nLeft*(i64)nRight>DIFF_ALIGN_MX + && (pCfg->diffFlags & DIFF_SLOW_SBS)==0 + ){ + if( (pCfg->diffFlags & DIFF_IGNORE_ALLWS)==0 ){ + unsigned char *aRes; + aRes = diffBlockAlignmentIgnoreSpace( + aLeft, nLeft,aRight, nRight,pCfg,pNResult); + if( aRes ) return aRes; + } + return diffBlockAlignmentDivideAndConquer( + aLeft, nLeft,aRight, nRight,pCfg,pNResult); + } + + /* If we reach this point, we will be doing an O(N*N) Wagner minimum + ** edit distance to compute the alignment. + */ + if( nRight < count(aBuf)-1 ){ + pToFree = 0; + a = aBuf; + }else{ + a = pToFree = fossil_malloc( sizeof(a[0])*(nRight+1) ); + } + aM = fossil_malloc( (nLeft+1)*(nRight+1) ); + + + /* Compute the best alignment */ + for(i=0; i<=nRight; i++){ + aM[i] = 2; + a[i] = i*50; + } + aM[0] = 0; + for(j=1; j<=nLeft; j++){ + int p = a[0]; + a[0] = p+50; + aM[j*(nRight+1)] = 1; + for(i=1; i<=nRight; i++){ + int m = a[i-1]+50; + int d = 2; + if( m>a[i]+50 ){ + m = a[i]+50; + d = 1; + } + if( m>p ){ + int score = match_dline(&aLeft[j-1], &aRight[i-1]); + if( (score<=90 || (i<j+1 && i>j-1)) && m>p+score ){ + m = p+score; + d = 3 | score*4; + } + } + p = a[i]; + a[i] = m; + aM[j*(nRight+1)+i] = d; + } + } + + /* Compute the lowest-cost path back through the matrix */ + i = nRight; + j = nLeft; + k = (nRight+1)*(nLeft+1)-1; + while( i+j>0 ){ + unsigned char c = aM[k]; + if( c>=3 ){ + assert( i>0 && j>0 ); + i--; + j--; + aM[k] = 3; + }else if( c==2 ){ + assert( i>0 ); + i--; + }else{ + assert( j>0 ); + j--; + } + k--; + aM[k] = aM[j*(nRight+1)+i]; + } + k++; + i = (nRight+1)*(nLeft+1) - k; + memmove(aM, &aM[k], i); + *pNResult = i; + + /* Return the result */ + fossil_free(pToFree); + return aM; +} + +/* +** R[] is an array of six integer, two COPY/DELETE/INSERT triples for a +** pair of adjacent differences. Return true if the gap between these +** two differences is so small that they should be rendered as a single +** edit. +*/ +static int smallGap(const int *R, int ma, int mb){ + int m = R[3]; + ma += R[4] + m; + mb += R[5] + m; + if( ma*mb>DIFF_ALIGN_MX ) return 0; + return m<=2 || m<=(R[1]+R[2]+R[4]+R[5])/8; +} + +/* +** Format a diff using a DiffBuilder object +*/ +static void formatDiff( + DContext *p, /* The computed diff */ + DiffConfig *pCfg, /* Configuration options */ + DiffBuilder *pBuilder /* The formatter object */ +){ + DLine *A; /* Left side of the diff */ + DLine *B; /* Right side of the diff */ + unsigned int a = 0; /* Index of next line in A[] */ + unsigned int b = 0; /* Index of next line in B[] */ + const int *R; /* Array of COPY/DELETE/INSERT triples */ + unsigned int r; /* Index into R[] */ + unsigned int nr; /* Number of COPY/DELETE/INSERT triples to process */ + unsigned int mxr; /* Maximum value for r */ + unsigned int i, j; /* Loop counters */ + unsigned int m, ma, mb;/* Number of lines to output */ + signed int skip = 0; /* Number of lines to skip */ + unsigned int nContext; /* Lines of context above and below each change */ + + nContext = diff_context_lines(pCfg); + A = p->aFrom; + B = p->aTo; + R = p->aEdit; + mxr = p->nEdit; + while( mxr>2 && R[mxr-1]==0 && R[mxr-2]==0 ){ mxr -= 3; } + + for(r=0; r<mxr; r += 3*nr){ + /* Figure out how many triples to show in a single block */ + for(nr=1; 3*nr<mxr && R[r+nr*3]>0 && R[r+nr*3]<(int)nContext*2; nr++){} + + /* If there is a regex, skip this block (generate no diff output) + ** if the regex matches or does not match both insert and delete. + ** Only display the block if one side matches but the other side does + ** not. + */ + if( pCfg->pRe ){ + int hideBlock = 1; + int xa = a, xb = b; + for(i=0; hideBlock && i<nr; i++){ + int c1, c2; + xa += R[r+i*3]; + xb += R[r+i*3]; + c1 = re_dline_match(pCfg->pRe, &A[xa], R[r+i*3+1]); + c2 = re_dline_match(pCfg->pRe, &B[xb], R[r+i*3+2]); + hideBlock = c1==c2; + xa += R[r+i*3+1]; + xb += R[r+i*3+2]; + } + if( hideBlock ){ + a = xa; + b = xb; + continue; + } + } + + /* Figure out how many lines of A and B are to be displayed + ** for this change block. + */ + if( R[r]>(int)nContext ){ + skip = R[r] - nContext; + }else{ + skip = 0; + } + /* Show the initial common area */ + a += skip; + b += skip; + m = R[r] - skip; + if( r ) skip -= nContext; + if( skip>0 ){ + if( skip<(int)nContext ){ + /* If the amount to skip is less that the context band, then + ** go ahead and show the skip band as it is not worth eliding */ + for(j=0; (int)j<skip; j++){ + pBuilder->xCommon(pBuilder, &A[a+j-skip]); + } + }else{ + pBuilder->xSkip(pBuilder, skip, 0); + } + } + for(j=0; j<m; j++){ + pBuilder->xCommon(pBuilder, &A[a+j]); + } + a += m; + b += m; + + /* Show the differences */ + for(i=0; i<nr; i++){ + int nAlign; + unsigned char *alignment; + ma = R[r+i*3+1]; /* Lines on left but not on right */ + mb = R[r+i*3+2]; /* Lines on right but not on left */ + + /* Try merging the current block with subsequent blocks, if the + ** subsequent blocks are nearby and there result isn't too big. + */ + while( i<nr-1 && smallGap(&R[r+i*3],ma,mb) ){ + i++; + m = R[r+i*3]; + ma += R[r+i*3+1] + m; + mb += R[r+i*3+2] + m; + } + + /* Try to find an alignment for the lines within this one block */ + alignment = diffBlockAlignment(&A[a], ma, &B[b], mb, pCfg, &nAlign); + + for(j=0; ma+mb>0; j++){ + assert( (int)j<nAlign ); + switch( alignment[j] ){ + case 1: { + /* Delete one line from the left */ + pBuilder->xDelete(pBuilder, &A[a]); + ma--; + a++; + break; + } + case 2: { + /* Insert one line on the right */ + pBuilder->xInsert(pBuilder, &B[b]); + assert( mb>0 ); + mb--; + b++; + break; + } + case 3: { + /* The left line is changed into the right line */ + if( p->xDiffer(&A[a], &B[b])==0 ){ + pBuilder->xCommon(pBuilder, &A[a]); + }else{ + pBuilder->xEdit(pBuilder, &A[a], &B[b]); + } + assert( ma>0 && mb>0 ); + ma--; + mb--; + a++; + b++; + break; + } + case 4: { + /* Delete from left then separately insert on the right */ + pBuilder->xReplace(pBuilder, &A[a], &B[b]); + ma--; + a++; + mb--; + b++; + break; + } + } + } + assert( nAlign==(int)j ); + fossil_free(alignment); + if( i<nr-1 ){ + m = R[r+i*3+3]; + for(j=0; j<m; j++){ + pBuilder->xCommon(pBuilder, &A[a+j]); + } + b += m; + a += m; + } + } + + /* Show the final common area */ + assert( nr==i ); + m = R[r+nr*3]; + if( m>nContext ) m = nContext; + for(j=0; j<m && j<nContext; j++){ + pBuilder->xCommon(pBuilder, &A[a+j]); + } + } + if( R[r]>(int)nContext ){ + pBuilder->xSkip(pBuilder, R[r] - nContext, 1); + } + pBuilder->xEnd(pBuilder); +} + + +/* +** Compute the optimal longest common subsequence (LCS) using an +** exhaustive search. This version of the LCS is only used for +** shorter input strings since runtime is O(N*N) where N is the +** input string length. +*/ +static void optimalLCS( + DContext *p, /* Two files being compared */ + int iS1, int iE1, /* Range of lines in p->aFrom[] */ + int iS2, int iE2, /* Range of lines in p->aTo[] */ + int *piSX, int *piEX, /* Write p->aFrom[] common segment here */ + int *piSY, int *piEY /* Write p->aTo[] common segment here */ +){ + int mxLength = 0; /* Length of longest common subsequence */ + int i, j; /* Loop counters */ + int k; /* Length of a candidate subsequence */ + int iSXb = iS1; /* Best match so far */ + int iSYb = iS2; /* Best match so far */ + + for(i=iS1; i<iE1-mxLength; i++){ + for(j=iS2; j<iE2-mxLength; j++){ + if( p->xDiffer(&p->aFrom[i], &p->aTo[j]) ) continue; + if( mxLength && p->xDiffer(&p->aFrom[i+mxLength], &p->aTo[j+mxLength]) ){ + continue; + } + k = 1; + while( i+k<iE1 && j+k<iE2 && p->xDiffer(&p->aFrom[i+k],&p->aTo[j+k])==0 ){ + k++; + } + if( k>mxLength ){ + iSXb = i; + iSYb = j; + mxLength = k; + } + } + } + *piSX = iSXb; + *piEX = iSXb + mxLength; + *piSY = iSYb; + *piEY = iSYb + mxLength; +} + +/* +** Compare two blocks of text on lines iS1 through iE1-1 of the aFrom[] +** file and lines iS2 through iE2-1 of the aTo[] file. Locate a sequence +** of lines in these two blocks that are exactly the same. Return +** the bounds of the matching sequence. +** +** If there are two or more possible answers of the same length, the +** returned sequence should be the one closest to the center of the +** input range. +** +** Ideally, the common sequence should be the longest possible common +** sequence. However, an exact computation of LCS is O(N*N) which is +** way too slow for larger files. So this routine uses an O(N) +** heuristic approximation based on hashing that usually works about +** as well. But if the O(N) algorithm doesn't get a good solution +** and N is not too large, we fall back to an exact solution by +** calling optimalLCS(). +*/ +static void longestCommonSequence( + DContext *p, /* Two files being compared */ + int iS1, int iE1, /* Range of lines in p->aFrom[] */ + int iS2, int iE2, /* Range of lines in p->aTo[] */ + int *piSX, int *piEX, /* Write p->aFrom[] common segment here */ + int *piSY, int *piEY /* Write p->aTo[] common segment here */ +){ + int i, j, k; /* Loop counters */ + int n; /* Loop limit */ + DLine *pA, *pB; /* Pointers to lines */ + int iSX, iSY, iEX, iEY; /* Current match */ + int skew = 0; /* How lopsided is the match */ + int dist = 0; /* Distance of match from center */ + int mid; /* Center of the chng */ + int iSXb, iSYb, iEXb, iEYb; /* Best match so far */ + int iSXp, iSYp, iEXp, iEYp; /* Previous match */ + sqlite3_int64 bestScore; /* Best score so far */ + sqlite3_int64 score; /* Score for current candidate LCS */ + int span; /* combined width of the input sequences */ + int cutoff = 4; /* Max hash chain entries to follow */ + int nextCutoff = -1; /* Value of cutoff for next iteration */ + + span = (iE1 - iS1) + (iE2 - iS2); + bestScore = -9223300000*(sqlite3_int64)1000000000; + score = 0; + iSXb = iSXp = iS1; + iEXb = iEXp = iS1; + iSYb = iSYp = iS2; + iEYb = iEYp = iS2; + mid = (iE1 + iS1)/2; + do{ + nextCutoff = 0; + for(i=iS1; i<iE1; i++){ + int limit = 0; + j = p->aTo[p->aFrom[i].h % p->nTo].iHash; + while( j>0 + && (j-1<iS2 || j>=iE2 || p->xDiffer(&p->aFrom[i], &p->aTo[j-1])) + ){ + if( limit++ > cutoff ){ + j = 0; + nextCutoff = cutoff*4; + break; + } + j = p->aTo[j-1].iNext; + } + if( j==0 ) continue; + assert( i>=iSXb && i>=iSXp ); + if( i<iEXb && j>=iSYb && j<iEYb ) continue; + if( i<iEXp && j>=iSYp && j<iEYp ) continue; + iSX = i; + iSY = j-1; + pA = &p->aFrom[iSX-1]; + pB = &p->aTo[iSY-1]; + n = minInt(iSX-iS1, iSY-iS2); + for(k=0; k<n && p->xDiffer(pA,pB)==0; k++, pA--, pB--){} + iSX -= k; + iSY -= k; + iEX = i+1; + iEY = j; + pA = &p->aFrom[iEX]; + pB = &p->aTo[iEY]; + n = minInt(iE1-iEX, iE2-iEY); + for(k=0; k<n && p->xDiffer(pA,pB)==0; k++, pA++, pB++){} + iEX += k; + iEY += k; + skew = (iSX-iS1) - (iSY-iS2); + if( skew<0 ) skew = -skew; + dist = (iSX+iEX)/2 - mid; + if( dist<0 ) dist = -dist; + score = (iEX - iSX)*(sqlite3_int64)span - (skew + dist); + if( score>bestScore ){ + bestScore = score; + iSXb = iSX; + iSYb = iSY; + iEXb = iEX; + iEYb = iEY; + }else if( iEX>iEXp ){ + iSXp = iSX; + iSYp = iSY; + iEXp = iEX; + iEYp = iEY; + } + } + }while( iSXb==iEXb && nextCutoff && (cutoff=nextCutoff)<=64 ); + if( iSXb==iEXb && (sqlite3_int64)(iE1-iS1)*(iE2-iS2)<2500 ){ + /* If no common sequence is found using the hashing heuristic and + ** the input is not too big, use the expensive exact solution */ + optimalLCS(p, iS1, iE1, iS2, iE2, piSX, piEX, piSY, piEY); + }else{ + *piSX = iSXb; + *piSY = iSYb; + *piEX = iEXb; + *piEY = iEYb; + } +} + +/* +** Expand the size of aEdit[] array to hold at least nEdit elements. +*/ +static void expandEdit(DContext *p, int nEdit){ + p->aEdit = fossil_realloc(p->aEdit, nEdit*sizeof(int)); + p->nEditAlloc = nEdit; +} + +/* +** Append a new COPY/DELETE/INSERT triple. +*/ +static void appendTriple(DContext *p, int nCopy, int nDel, int nIns){ + /* printf("APPEND %d/%d/%d\n", nCopy, nDel, nIns); */ + if( p->nEdit>=3 ){ + if( p->aEdit[p->nEdit-1]==0 ){ + if( p->aEdit[p->nEdit-2]==0 ){ + p->aEdit[p->nEdit-3] += nCopy; + p->aEdit[p->nEdit-2] += nDel; + p->aEdit[p->nEdit-1] += nIns; + return; + } + if( nCopy==0 ){ + p->aEdit[p->nEdit-2] += nDel; + p->aEdit[p->nEdit-1] += nIns; + return; + } + } + if( nCopy==0 && nDel==0 ){ + p->aEdit[p->nEdit-1] += nIns; + return; + } + } + if( p->nEdit+3>p->nEditAlloc ){ + expandEdit(p, p->nEdit*2 + 15); + if( p->aEdit==0 ) return; + } + p->aEdit[p->nEdit++] = nCopy; + p->aEdit[p->nEdit++] = nDel; + p->aEdit[p->nEdit++] = nIns; +} + +/* +** A common subsequence between p->aFrom and p->aTo has been found. +** This routine tries to judge if the subsequence really is a valid +** match or rather is just an artifact of an indentation change. +** +** Return non-zero if the subsequence is valid. Return zero if the +** subsequence seems likely to be an editing artifact and should be +** ignored. +** +** This routine is a heuristic optimization intended to give more +** intuitive diff results following an indentation change it code that +** is formatted similarly to C/C++, Javascript, Go, TCL, and similar +** languages that use {...} for nesting. A correct diff is computed +** even if this routine always returns true (non-zero). But sometimes +** a more intuitive diff can result if this routine returns false. +** +** The subsequences consists of the rows iSX through iEX-1 (inclusive) +** in p->aFrom[]. The total sequences is iS1 through iE1-1 (inclusive) +** of p->aFrom[]. +** +** Example where this heuristic is useful, see the diff at +** https://www.sqlite.org/src/fdiff?v1=0e79dd15cbdb4f48&v2=33955a6fd874dd97 +** +** See also discussion at https://fossil-scm.org/forum/forumpost/9ba3284295 +** +** ALGORITHM (subject to change and refinement): +** +** 1. If the subsequence is larger than 1/7th of the original span, +** then consider it valid. --> return 1 +** +** 2. If no lines of the subsequence contains more than one +** non-whitespace character, --> return 0 +** +** 3. If any line of the subsequence contains more than one non-whitespace +** character and is unique across the entire sequence after ignoring +** leading and trailing whitespace --> return 1 +** +** 4. Otherwise, it is potentially an artifact of an indentation +** change. --> return 0 +*/ +static int likelyNotIndentChngArtifact( + DContext *p, /* The complete diff context */ + int iS1, /* Start of the main segment */ + int iSX, /* Start of the subsequence */ + int iEX, /* First row past the end of the subsequence */ + int iE1 /* First row past the end of the main segment */ +){ + int i, j, n; + + /* Rule (1) */ + if( (iEX-iSX)*7 >= (iE1-iS1) ) return 1; + + /* Compute DLine.indent and DLine.nw for all lines of the subsequence. + ** If no lines contain more than one non-whitespace character return + ** 0 because the subsequence could be due to an indentation change. + ** Rule (2). + */ + n = 0; + for(i=iSX; i<iEX; i++){ + DLine *pA = &p->aFrom[i]; + if( pA->nw==0 && pA->n ){ + const char *zA = pA->z; + const int nn = pA->n; + int ii, jj; + for(ii=0; ii<nn && diff_isspace(zA[ii]); ii++){} + pA->indent = ii; + for(jj=nn-1; jj>ii && diff_isspace(zA[jj]); jj--){} + pA->nw = jj - ii + 1; + } + if( pA->nw>1 ) n++; + } + if( n==0 ) return 0; + + /* Compute DLine.indent and DLine.nw for the entire sequence */ + for(i=iS1; i<iE1; i++){ + DLine *pA; + if( i==iSX ){ + i = iEX; + if( i>=iE1 ) break; + } + pA = &p->aFrom[i]; + if( pA->nw==0 && pA->n ){ + const char *zA = pA->z; + const int nn = pA->n; + int ii, jj; + for(ii=0; ii<nn && diff_isspace(zA[ii]); ii++){} + pA->indent = ii; + for(jj=nn-1; jj>ii && diff_isspace(zA[jj]); jj--){} + pA->nw = jj - ii + 1; + } + } + + /* Check to see if any subsequence line that has more than one + ** non-whitespace character is unique across the entire sequence. + ** Rule (3) + */ + for(i=iSX; i<iEX; i++){ + const char *z = p->aFrom[i].z + p->aFrom[i].indent; + const int nw = p->aFrom[i].nw; + if( nw<=1 ) continue; + for(j=iS1; j<iSX; j++){ + if( p->aFrom[j].nw!=nw ) continue; + if( memcmp(p->aFrom[j].z+p->aFrom[j].indent,z,nw)==0 ) break; + } + if( j<iSX ) continue; + for(j=iEX; j<iE1; j++){ + if( p->aFrom[j].nw!=nw ) continue; + if( memcmp(p->aFrom[j].z+p->aFrom[j].indent,z,nw)==0 ) break; + } + if( j>=iE1 ) break; + } + return i<iEX; +} + + +/* +** Do a single step in the difference. Compute a sequence of +** copy/delete/insert steps that will convert lines iS1 through iE1-1 of +** the input into lines iS2 through iE2-1 of the output and write +** that sequence into the difference context. +** +** The algorithm is to find a block of common text near the middle +** of the two segments being diffed. Then recursively compute +** differences on the blocks before and after that common segment. +** Special cases apply if either input segment is empty or if the +** two segments have no text in common. +*/ +static void diff_step(DContext *p, int iS1, int iE1, int iS2, int iE2){ + int iSX, iEX, iSY, iEY; + + if( iE1<=iS1 ){ + /* The first segment is empty */ + if( iE2>iS2 ){ + appendTriple(p, 0, 0, iE2-iS2); + } + return; + } + if( iE2<=iS2 ){ + /* The second segment is empty */ + appendTriple(p, 0, iE1-iS1, 0); + return; + } + + /* Find the longest matching segment between the two sequences */ + longestCommonSequence(p, iS1, iE1, iS2, iE2, &iSX, &iEX, &iSY, &iEY); + + if( iEX>iSX+5 + || (iEX>iSX && likelyNotIndentChngArtifact(p,iS1,iSX,iEX,iE1) ) + ){ + /* A common segment has been found. + ** Recursively diff either side of the matching segment */ + diff_step(p, iS1, iSX, iS2, iSY); + if( iEX>iSX ){ + appendTriple(p, iEX - iSX, 0, 0); + } + diff_step(p, iEX, iE1, iEY, iE2); + }else{ + /* The two segments have nothing in common. Delete the first then + ** insert the second. */ + appendTriple(p, 0, iE1-iS1, iE2-iS2); + } +} + +/* +** Compute the differences between two files already loaded into +** the DContext structure. +** +** A divide and conquer technique is used. We look for a large +** block of common text that is in the middle of both files. Then +** compute the difference on those parts of the file before and +** after the common block. This technique is fast, but it does +** not necessarily generate the minimum difference set. On the +** other hand, we do not need a minimum difference set, only one +** that makes sense to human readers, which this algorithm does. +** +** Any common text at the beginning and end of the two files is +** removed before starting the divide-and-conquer algorithm. +*/ +static void diff_all(DContext *p){ + int mnE, iS, iE1, iE2; + + /* Carve off the common header and footer */ + iE1 = p->nFrom; + iE2 = p->nTo; + while( iE1>0 && iE2>0 && p->xDiffer(&p->aFrom[iE1-1], &p->aTo[iE2-1])==0 ){ + iE1--; + iE2--; + } + mnE = iE1<iE2 ? iE1 : iE2; + for(iS=0; iS<mnE && p->xDiffer(&p->aFrom[iS],&p->aTo[iS])==0; iS++){} + + /* do the difference */ + if( iS>0 ){ + appendTriple(p, iS, 0, 0); + } + diff_step(p, iS, iE1, iS, iE2); + if( iE1<p->nFrom ){ + appendTriple(p, p->nFrom - iE1, 0, 0); + } + + /* Terminate the COPY/DELETE/INSERT triples with three zeros */ + expandEdit(p, p->nEdit+3); + if( p->aEdit ){ + p->aEdit[p->nEdit++] = 0; + p->aEdit[p->nEdit++] = 0; + p->aEdit[p->nEdit++] = 0; + } +} + +/* +** Attempt to shift insertion or deletion blocks so that they begin and +** end on lines that are pure whitespace. In other words, try to transform +** this: +** +** int func1(int x){ +** return x*10; +** +} +** + +** +int func2(int x){ +** + return x*20; +** } +** +** int func3(int x){ +** return x/5; +** } +** +** Into one of these: +** +** int func1(int x){ int func1(int x){ +** return x*10; return x*10; +** } } +** + +** +int func2(int x){ +int func2(int x){ +** + return x*20; + return x*20; +** +} +} +** + +** int func3(int x){ int func3(int x){ +** return x/5; return x/5; +** } } +*/ +static void diff_optimize(DContext *p){ + int r; /* Index of current triple */ + int lnFrom; /* Line number in p->aFrom */ + int lnTo; /* Line number in p->aTo */ + int cpy, del, ins; + + lnFrom = lnTo = 0; + for(r=0; r<p->nEdit; r += 3){ + cpy = p->aEdit[r]; + del = p->aEdit[r+1]; + ins = p->aEdit[r+2]; + lnFrom += cpy; + lnTo += cpy; + + /* Shift insertions toward the beginning of the file */ + while( cpy>0 && del==0 && ins>0 ){ + DLine *pTop = &p->aFrom[lnFrom-1]; /* Line before start of insert */ + DLine *pBtm = &p->aTo[lnTo+ins-1]; /* Last line inserted */ + if( p->xDiffer(pTop, pBtm) ) break; + if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break; + lnFrom--; + lnTo--; + p->aEdit[r]--; + p->aEdit[r+3]++; + cpy--; + } + + /* Shift insertions toward the end of the file */ + while( r+3<p->nEdit && p->aEdit[r+3]>0 && del==0 && ins>0 ){ + DLine *pTop = &p->aTo[lnTo]; /* First line inserted */ + DLine *pBtm = &p->aTo[lnTo+ins]; /* First line past end of insert */ + if( p->xDiffer(pTop, pBtm) ) break; + if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop+1)+LENGTH(pBtm) ) break; + lnFrom++; + lnTo++; + p->aEdit[r]++; + p->aEdit[r+3]--; + cpy++; + } + + /* Shift deletions toward the beginning of the file */ + while( cpy>0 && del>0 && ins==0 ){ + DLine *pTop = &p->aFrom[lnFrom-1]; /* Line before start of delete */ + DLine *pBtm = &p->aFrom[lnFrom+del-1]; /* Last line deleted */ + if( p->xDiffer(pTop, pBtm) ) break; + if( LENGTH(pTop+1)+LENGTH(pBtm)<=LENGTH(pTop)+LENGTH(pBtm-1) ) break; + lnFrom--; + lnTo--; + p->aEdit[r]--; + p->aEdit[r+3]++; + cpy--; + } + + /* Shift deletions toward the end of the file */ + while( r+3<p->nEdit && p->aEdit[r+3]>0 && del>0 && ins==0 ){ + DLine *pTop = &p->aFrom[lnFrom]; /* First line deleted */ + DLine *pBtm = &p->aFrom[lnFrom+del]; /* First line past end of delete */ + if( p->xDiffer(pTop, pBtm) ) break; + if( LENGTH(pTop)+LENGTH(pBtm-1)<=LENGTH(pTop)+LENGTH(pBtm) ) break; + lnFrom++; + lnTo++; + p->aEdit[r]++; + p->aEdit[r+3]--; + cpy++; + } + + lnFrom += del; + lnTo += ins; + } +} + +/* +** Extract the number of lines of context from diffFlags. Supply an +** appropriate default if no context width is specified. If pCfg is +** NULL then the compile-time default is used (which gets propagated +** to JS-side state by certain pages). +*/ +int diff_context_lines(DiffConfig *pCfg){ + const int dflt = 5; + if(pCfg!=0){ + int n = pCfg->nContext; + if( n==0 && (pCfg->diffFlags & DIFF_CONTEXT_EX)==0 ) n = dflt; + return n<0 ? 0x7ffffff : n; + }else{ + return dflt; + } +} + +/* +** Extract the width of columns for side-by-side diff. Supply an +** appropriate default if no width is given. +** +** Calculate the default automatically, based on terminal's current width: +** term-width = 2*diff-col + diff-marker + 1 +** diff-col = lineno + lmargin + text-width + rmargin +** +** text-width = (term-width - diff-marker - 1)/2 - lineno - lmargin - rmargin +*/ +int diff_width(DiffConfig *pCfg){ + int w = pCfg->wColumn; + if( w==0 ){ + static struct { + unsigned int lineno, lmargin, text, rmargin, marker; + } sbsW = { 5, 2, 0, 0, 3 }; + const unsigned int wMin = 24, wMax = 132; + unsigned int tw = terminal_get_width(80); + unsigned int twMin = + (wMin + sbsW.lineno + sbsW.lmargin + sbsW.rmargin)*2 + sbsW.marker + 1; + unsigned int twMax = + (wMax + sbsW.lineno + sbsW.lmargin + sbsW.rmargin)*2 + sbsW.marker + 1; + + if( tw<twMin ){ + tw = twMin; + }else if( tw>twMax ){ + tw = twMax; + } + sbsW.text = + (tw - sbsW.marker - 1)/2 - sbsW.lineno - sbsW.lmargin - sbsW.rmargin; + w = sbsW.text; + } + return w; +} + +/* +** Append the error message to pOut. +*/ +void diff_errmsg(Blob *pOut, const char *msg, int diffFlags){ + if( diffFlags & DIFF_HTML ){ + blob_appendf(pOut, "<p class=\"generalError\">%s</p>", msg); + }else{ + blob_append(pOut, msg, -1); + } +} + +/* +** Generate a report of the differences between files pA_Blob and pB_Blob. +** +** If pOut!=NULL then append text to pOut that will be the difference, +** formatted according to flags in diffFlags. The pOut Blob must have +** already been initialized. +** +** If pOut==NULL then no formatting occurs. Instead, this routine +** returns a pointer to an array of integers. The integers come in +** triples. The elements of each triple are: +** +** 1. The number of lines to copy +** 2. The number of lines to delete +** 3. The number of lines to insert +** +** The return vector is terminated by a triple of all zeros. The caller +** should free the returned vector using fossil_free(). +** +** This diff utility does not work on binary files. If a binary +** file is encountered, 0 is returned and pOut is written with +** text "cannot compute difference between binary files". +*/ +int *text_diff( + Blob *pA_Blob, /* FROM file */ + Blob *pB_Blob, /* TO file */ + Blob *pOut, /* Write diff here if not NULL */ + DiffConfig *pCfg /* Configuration options */ +){ + int ignoreWs; /* Ignore whitespace */ + DContext c; + int nDel = 0, nIns = 0; + + if( pCfg->diffFlags & DIFF_INVERT ){ + Blob *pTemp = pA_Blob; + pA_Blob = pB_Blob; + pB_Blob = pTemp; + } + ignoreWs = (pCfg->diffFlags & DIFF_IGNORE_ALLWS)!=0; + blob_to_utf8_no_bom(pA_Blob, 0); + blob_to_utf8_no_bom(pB_Blob, 0); + + /* Prepare the input files */ + memset(&c, 0, sizeof(c)); + if( (pCfg->diffFlags & DIFF_IGNORE_ALLWS)==DIFF_IGNORE_ALLWS ){ + c.xDiffer = compare_dline_ignore_allws; + }else{ + c.xDiffer = compare_dline; + } + if( pCfg->diffFlags & DIFF_BY_TOKEN ){ + c.aFrom = break_into_tokens(blob_str(pA_Blob), blob_size(pA_Blob), + &c.nFrom, pCfg->diffFlags); + c.aTo = break_into_tokens(blob_str(pB_Blob), blob_size(pB_Blob), + &c.nTo, pCfg->diffFlags); + }else{ + c.aFrom = break_into_lines(blob_str(pA_Blob), blob_size(pA_Blob), + &c.nFrom, pCfg->diffFlags); + c.aTo = break_into_lines(blob_str(pB_Blob), blob_size(pB_Blob), + &c.nTo, pCfg->diffFlags); + } + if( c.aFrom==0 || c.aTo==0 ){ + fossil_free(c.aFrom); + fossil_free(c.aTo); + if( pOut ){ + diff_errmsg(pOut, DIFF_CANNOT_COMPUTE_BINARY, pCfg->diffFlags); + } + return 0; + } + + /* Compute the difference */ + diff_all(&c); + if( ignoreWs && c.nEdit==6 && c.aEdit[1]==0 && c.aEdit[2]==0 ){ + fossil_free(c.aFrom); + fossil_free(c.aTo); + fossil_free(c.aEdit); + if( pOut ) diff_errmsg(pOut, DIFF_WHITESPACE_ONLY, pCfg->diffFlags); + return 0; + } + if( (pCfg->diffFlags & DIFF_NOTTOOBIG)!=0 ){ + int i, m, n; + int *a = c.aEdit; + int mx = c.nEdit; + for(i=m=n=0; i<mx; i+=3){ m += a[i]; n += a[i+1]+a[i+2]; } + if( n>10000 ){ + fossil_free(c.aFrom); + fossil_free(c.aTo); + fossil_free(c.aEdit); + if( pOut ) diff_errmsg(pOut, DIFF_TOO_MANY_CHANGES, pCfg->diffFlags); + return 0; + } + } + if( (pCfg->diffFlags & DIFF_NOOPT)==0 ){ + diff_optimize(&c); + } + if( (pCfg->diffFlags & DIFF_BY_TOKEN)!=0 ){ + /* Convert token counts into byte counts. */ + int i; + int iA = 0; + int iB = 0; + for(i=0; c.aEdit[i] || c.aEdit[i+1] || c.aEdit[i+2]; i+=3){ + int k, sum; + for(k=0, sum=0; k<c.aEdit[i]; k++) sum += c.aFrom[iA++].n; + iB += c.aEdit[i]; + c.aEdit[i] = sum; + for(k=0, sum=0; k<c.aEdit[i+1]; k++) sum += c.aFrom[iA++].n; + c.aEdit[i+1] = sum; + for(k=0, sum=0; k<c.aEdit[i+2]; k++) sum += c.aTo[iB++].n; + c.aEdit[i+2] = sum; + } + } + + if( pCfg->diffFlags & DIFF_NUMSTAT ){ + int i; + for(i=0; c.aEdit[i] || c.aEdit[i+1] || c.aEdit[i+2]; i+=3){ + nDel += c.aEdit[i+1]; + nIns += c.aEdit[i+2]; + } + g.diffCnt[1] += nIns; + g.diffCnt[2] += nDel; + if( nIns+nDel ){ + g.diffCnt[0]++; + } + } + + if( pOut ){ + if( pCfg->diffFlags & DIFF_NUMSTAT && !(pCfg->diffFlags & DIFF_HTML)){ + if( nIns+nDel ){ + if( !(pCfg->diffFlags & DIFF_BRIEF) ){ + blob_appendf(pOut, "%10d %10d", nIns, nDel); + } + } + }else if( pCfg->diffFlags & (DIFF_RAW|DIFF_BY_TOKEN) ){ + const int *R = c.aEdit; + unsigned int r; + for(r=0; R[r] || R[r+1] || R[r+2]; r += 3){ + blob_appendf(pOut, " copy %6d delete %6d insert %6d\n", + R[r], R[r+1], R[r+2]); + } + }else if( pCfg->diffFlags & DIFF_JSON ){ + DiffBuilder *pBuilder = dfjsonNew(pOut); + formatDiff(&c, pCfg, pBuilder); + blob_append_char(pOut, '\n'); + }else if( pCfg->diffFlags & DIFF_TCL ){ + DiffBuilder *pBuilder = dftclNew(pOut); + formatDiff(&c, pCfg, pBuilder); + }else if( pCfg->diffFlags & DIFF_SIDEBYSIDE ){ + DiffBuilder *pBuilder; + if( pCfg->diffFlags & DIFF_HTML ){ + pBuilder = dfsplitNew(pOut, pCfg); + }else{ + pBuilder = dfsbsNew(pOut, pCfg); + } + formatDiff(&c, pCfg, pBuilder); + }else if( pCfg->diffFlags & DIFF_DEBUG ){ + DiffBuilder *pBuilder = dfdebugNew(pOut); + formatDiff(&c, pCfg, pBuilder); + }else if( pCfg->diffFlags & DIFF_HTML ){ + DiffBuilder *pBuilder = dfunifiedNew(pOut, pCfg); + formatDiff(&c, pCfg, pBuilder); + }else{ + contextDiff(&c, pOut, pCfg); + } + fossil_free(c.aFrom); + fossil_free(c.aTo); + fossil_free(c.aEdit); + return 0; + }else{ + /* If a context diff is not requested, then return the + ** array of COPY/DELETE/INSERT triples. + */ + free(c.aFrom); + free(c.aTo); + return c.aEdit; + } +} + +/* +** Initialize the DiffConfig object using command-line options. +** +** Process diff-related command-line options and return an appropriate +** "diffFlags" integer. +** +** -b|--browser Show the diff output in a web-browser +** --brief Show filenames only DIFF_BRIEF +** --by Shorthand for "--browser -y" +** -c|--context N N lines of context. nContext +** --dark Use dark mode for Tcl/Tk and HTML output +** --html Format for HTML DIFF_HTML +** -i|--internal Use built-in diff, not an external tool +** --invert Invert the diff DIFF_INVERT +** --json Output formatted as JSON +** --label NAME Column label. Can be repeated once. +** -n|--linenum Show line numbers DIFF_LINENO +** -N|--new-file Alias for --verbose +** --noopt Disable optimization DIFF_NOOPT +** -s|--numstat Show change counts DIFF_NUMSTAT +** --strip-trailing-cr Strip trailing CR DIFF_STRIP_EOLCR +** --tcl Tcl-formatted output used internally by --tk +** --unified Unified diff. ~DIFF_SIDEBYSIDE +** -v|--verbose Show complete text of added or deleted files +** -w|--ignore-all-space Ignore all whitespaces DIFF_IGNORE_ALLWS +** --webpage Format output as a stand-alone HTML webpage +** -W|--width N N character lines. wColumn +** -y|--side-by-side Side-by-side diff. DIFF_SIDEBYSIDE +** -Z|--ignore-trailing-space Ignore eol-whitespaces DIFF_IGNORE_EOLWS +*/ +void diff_options(DiffConfig *pCfg, int isGDiff, int bUnifiedTextOnly){ + u64 diffFlags = 0; + const char *z; + int f; + + memset(pCfg, 0, sizeof(*pCfg)); + if( find_option("ignore-trailing-space","Z",0)!=0 ){ + diffFlags = DIFF_IGNORE_EOLWS; + } + if( find_option("ignore-all-space","w",0)!=0 ){ + diffFlags = DIFF_IGNORE_ALLWS; /* stronger than DIFF_IGNORE_EOLWS */ + } + if( find_option("strip-trailing-cr",0,0)!=0 ){ + diffFlags |= DIFF_STRIP_EOLCR; + } + if( !bUnifiedTextOnly ){ + if( find_option("side-by-side","y",0)!=0 ) diffFlags |= DIFF_SIDEBYSIDE; + if( find_option("yy",0,0)!=0 ){ + diffFlags |= DIFF_SIDEBYSIDE | DIFF_SLOW_SBS; + } + if( find_option("html",0,0)!=0 ) diffFlags |= DIFF_HTML; + if( find_option("unified",0,0)!=0 ) diffFlags &= ~DIFF_SIDEBYSIDE; + if( find_option("webpage",0,0)!=0 ){ + diffFlags |= DIFF_HTML|DIFF_WEBPAGE|DIFF_LINENO; + } + if( find_option("browser","b",0)!=0 ){ + diffFlags |= DIFF_HTML|DIFF_WEBPAGE|DIFF_LINENO|DIFF_BROWSER; + } + if( find_option("by",0,0)!=0 ){ + diffFlags |= DIFF_HTML|DIFF_WEBPAGE|DIFF_LINENO|DIFF_BROWSER + |DIFF_SIDEBYSIDE; + } + if( find_option("json",0,0)!=0 ){ + diffFlags |= DIFF_JSON; + } + if( find_option("tcl",0,0)!=0 ){ + diffFlags |= DIFF_TCL; + } + + /* Undocumented and unsupported flags used for development + ** debugging and analysis: */ + if( find_option("debug",0,0)!=0 ) diffFlags |= DIFF_DEBUG; + if( find_option("raw",0,0)!=0 ) diffFlags |= DIFF_RAW; + if( find_option("bytoken",0,0)!=0 ){ + diffFlags = DIFF_RAW|DIFF_BY_TOKEN; + } + } + if( (z = find_option("context","c",1))!=0 ){ + char *zEnd; + f = (int)strtol(z, &zEnd, 10); + if( zEnd[0]==0 && errno!=ERANGE ){ + pCfg->nContext = f; + diffFlags |= DIFF_CONTEXT_EX; + } + } + if( (z = find_option("width","W",1))!=0 && (f = atoi(z))>0 ){ + pCfg->wColumn = f; + } + pCfg->azLabel[0] = find_option("label",0,1); + if( pCfg->azLabel[0] ){ + pCfg->azLabel[1] = find_option("label",0,1); + } + if( find_option("linenum","n",0)!=0 ) diffFlags |= DIFF_LINENO; + if( find_option("noopt",0,0)!=0 ) diffFlags |= DIFF_NOOPT; + if( find_option("numstat","s",0)!=0 ) diffFlags |= DIFF_NUMSTAT; + if( find_option("versions","h",0)!=0 ) diffFlags |= DIFF_SHOW_VERS; + if( find_option("dark",0,0)!=0 ) diffFlags |= DIFF_DARKMODE; + if( find_option("invert",0,0)!=0 ) diffFlags |= DIFF_INVERT; + if( find_option("brief",0,0)!=0 ) diffFlags |= DIFF_BRIEF; + if( find_option("internal","i",0)==0 + && (diffFlags & (DIFF_HTML|DIFF_TCL|DIFF_DEBUG|DIFF_JSON))==0 + ){ + pCfg->zDiffCmd = find_option("command", 0, 1); + if( pCfg->zDiffCmd==0 ) pCfg->zDiffCmd = diff_command_external(isGDiff); + if( pCfg->zDiffCmd ){ + const char *zDiffBinary; + pCfg->zBinGlob = diff_get_binary_glob(); + zDiffBinary = find_option("diff-binary", 0, 1); + if( zDiffBinary ){ + if( is_truth(zDiffBinary) ) diffFlags |= DIFF_INCBINARY; + }else if( db_get_boolean("diff-binary", 1) ){ + diffFlags |= DIFF_INCBINARY; + } + }else if( isGDiff) { + /* No external gdiff command found, using --by */ + diffFlags |= DIFF_HTML|DIFF_WEBPAGE|DIFF_LINENO|DIFF_BROWSER + |DIFF_SIDEBYSIDE; + } + } + if( find_option("verbose","v",0)!=0 ) diffFlags |= DIFF_VERBOSE; + /* Deprecated, but retained for script compatibility. */ + else if( find_option("new-file","N",0)!=0 ) diffFlags |= DIFF_VERBOSE; + + pCfg->diffFlags = diffFlags; +} + +/* +** COMMAND: test-diff +** COMMAND: xdiff +** +** Usage: %fossil xdiff [options] FILE1 FILE2 +** +** Compute an "external diff" between two files. By "external diff" we mean +** a diff between two disk files that are not necessarily under management. +** In other words, this command provides a mechanism to use Fossil's file +** difference engine on arbitrary disk files. See the "diff" command for +** computing differences between files that are under management. +** +** This command prints the differences between the two files FILE1 and FILE2. +** all of the usual diff formatting options (--tk, --by, -c N, etc.) apply. +** See the "diff" command for a full list of command-line options. +*/ +void xdiff_cmd(void){ + Blob a, b, out; + const char *zRe; /* Regex filter for diff output */ + DiffConfig DCfg; + + if( find_option("tk",0,0)!=0 ){ + diff_tk("xdiff", 2); + return; + } + find_option("i",0,0); + find_option("v",0,0); + diff_options(&DCfg, 0, 0); + zRe = find_option("regexp","e",1); + if( zRe ){ + const char *zErr = fossil_re_compile(&DCfg.pRe, zRe, 0); + if( zErr ) fossil_fatal("regex error: %s", zErr); + } + verify_all_options(); + if( g.argc!=4 ) usage("FILE1 FILE2"); + blob_zero(&out); + diff_begin(&DCfg); + diff_print_filenames(g.argv[2], g.argv[3], &DCfg, &out); + blob_read_from_file(&a, g.argv[2], ExtFILE); + blob_read_from_file(&b, g.argv[3], ExtFILE); + text_diff(&a, &b, &out, &DCfg); + blob_write_to_file(&out, "-"); + diff_end(&DCfg, 0); + re_free(DCfg.pRe); +} + +/* +** COMMAND: fdiff +** +** Usage: %fossil fdiff [options] HASH1 HASH2 +** +** Compute a diff between two artifacts in a repository (either a repository +** identified by the "-R FILENAME" option, or the repository that contains +** the working directory). +** +** All of the usual diff formatting options (--tk, --by, -c N, etc.) apply. +** See the "diff" command for a full list of command-line options. +*/ +void fdiff_cmd(void){ + Blob a, b, out; + const char *zRe; /* Regex filter for diff output */ + int rid; + DiffConfig DCfg; + + if( find_option("tk",0,0)!=0 ){ + diff_tk("fdiff", 2); + return; + } + find_option("i",0,0); + find_option("v",0,0); + diff_options(&DCfg, 0, 0); + zRe = find_option("regexp","e",1); + if( zRe ){ + const char *zErr = fossil_re_compile(&DCfg.pRe, zRe, 0); + if( zErr ) fossil_fatal("regex error: %s", zErr); + } + db_find_and_open_repository(0, 0); + verify_all_options(); + if( g.argc!=4 ) usage("HASH1 HASH2"); + blob_zero(&out); + diff_begin(&DCfg); + diff_print_filenames(g.argv[2], g.argv[3], &DCfg, &out); + rid = name_to_typed_rid(g.argv[2], 0); + content_get(rid, &a); + rid = name_to_typed_rid(g.argv[3], 0); + content_get(rid, &b); + text_diff(&a, &b, &out, &DCfg); + blob_write_to_file(&out, "-"); + diff_end(&DCfg, 0); + re_free(DCfg.pRe); +} + +/************************************************************************** +** The basic difference engine is above. What follows is the annotation +** engine. Both are in the same file since they share many components. +*/ + +/* +** The status of an annotation operation is recorded by an instance +** of the following structure. +*/ +typedef struct Annotator Annotator; +struct Annotator { + DContext c; /* The diff-engine context */ + struct AnnLine { /* Lines of the original files... */ + const char *z; /* The text of the line */ + short int n; /* Number of bytes (omitting trailing \n) */ + short int iVers; /* Level at which tag was set */ + } *aOrig; + int nOrig; /* Number of elements in aOrig[] */ + int nVers; /* Number of versions analyzed */ + int bMoreToDo; /* True if the limit was reached */ + int origId; /* RID for the zOrigin version */ + int showId; /* RID for the version being analyzed */ + struct AnnVers { + const char *zFUuid; /* File being analyzed */ + const char *zMUuid; /* Check-in containing the file */ + const char *zDate; /* Date of the check-in */ + const char *zBgColor; /* Suggested background color */ + const char *zUser; /* Name of user who did the check-in */ + unsigned cnt; /* Number of lines contributed by this check-in */ + } *aVers; /* For each check-in analyzed */ + char **azVers; /* Names of versions analyzed */ +}; + +/* +** Initialize the annotation process by specifying the file that is +** to be annotated. The annotator takes control of the input Blob and +** will release it when it is finished with it. +*/ +static int annotation_start(Annotator *p, Blob *pInput, u64 diffFlags){ + int i; + + memset(p, 0, sizeof(*p)); + if( (diffFlags & DIFF_IGNORE_ALLWS)==DIFF_IGNORE_ALLWS ){ + p->c.xDiffer = compare_dline_ignore_allws; + }else{ + p->c.xDiffer = compare_dline; + } + p->c.aTo = break_into_lines(blob_str(pInput), blob_size(pInput),&p->c.nTo, + diffFlags); + if( p->c.aTo==0 ){ + return 1; + } + p->aOrig = fossil_malloc( sizeof(p->aOrig[0])*p->c.nTo ); + for(i=0; i<p->c.nTo; i++){ + p->aOrig[i].z = p->c.aTo[i].z; + p->aOrig[i].n = p->c.aTo[i].n; + p->aOrig[i].iVers = -1; + } + p->nOrig = p->c.nTo; + return 0; +} + +/* +** The input pParent is the next most recent ancestor of the file +** being annotated. Do another step of the annotation. Return true +** if additional annotation is required. +*/ +static int annotation_step( + Annotator *p, + Blob *pParent, + int iVers, + u64 diffFlags +){ + int i, j; + int lnTo; + + /* Prepare the parent file to be diffed */ + p->c.aFrom = break_into_lines(blob_str(pParent), blob_size(pParent), + &p->c.nFrom, diffFlags); + if( p->c.aFrom==0 ){ + return 1; + } + + /* Compute the differences going from pParent to the file being + ** annotated. */ + diff_all(&p->c); + + /* Where new lines are inserted on this difference, record the + ** iVers as the source of the new line. + */ + for(i=lnTo=0; i<p->c.nEdit; i+=3){ + int nCopy = p->c.aEdit[i]; + int nIns = p->c.aEdit[i+2]; + lnTo += nCopy; + for(j=0; j<nIns; j++, lnTo++){ + if( p->aOrig[lnTo].iVers<0 ){ + p->aOrig[lnTo].iVers = iVers; + } + } + } + + /* Clear out the diff results */ + fossil_free(p->c.aEdit); + p->c.aEdit = 0; + p->c.nEdit = 0; + p->c.nEditAlloc = 0; + + /* Clear out the from file */ + free(p->c.aFrom); + + /* Return no errors */ + return 0; +} + +/* Return the current time as milliseconds since the Julian epoch */ +static sqlite3_int64 current_time_in_milliseconds(void){ + static sqlite3_vfs *clockVfs = 0; + sqlite3_int64 t; + if( clockVfs==0 ) clockVfs = sqlite3_vfs_find(0); + if( clockVfs->iVersion>=2 && clockVfs->xCurrentTimeInt64!=0 ){ + clockVfs->xCurrentTimeInt64(clockVfs, &t); + }else{ + double r; + clockVfs->xCurrentTime(clockVfs, &r); + t = (sqlite3_int64)(r*86400000.0); + } + return t; +} + +/* +** Compute a complete annotation on a file. The file is identified by its +** filename and check-in name (NULL for current check-in). +*/ +static void annotate_file( + Annotator *p, /* The annotator */ + const char *zFilename, /* The name of the file to be annotated */ + const char *zRevision, /* Use the version of the file in this check-in */ + const char *zLimit, /* Limit the number of versions analyzed */ + const char *zOrigin, /* The origin check-in, or NULL for root-of-tree */ + u64 annFlags /* Flags to alter the annotation */ +){ + Blob toAnnotate; /* Text of the final (mid) version of the file */ + Blob step; /* Text of previous revision */ + int cid; /* Selected check-in ID */ + int origid = 0; /* The origin ID or zero */ + int rid; /* Artifact ID of the file being annotated */ + int fnid; /* Filename ID */ + Stmt q; /* Query returning all ancestor versions */ + int cnt = 0; /* Number of versions analyzed */ + int iLimit; /* Maximum number of versions to analyze */ + sqlite3_int64 mxTime; /* Halt at this time if not already complete */ + + memset(p, 0, sizeof(*p)); + + if( zLimit ){ + if( strcmp(zLimit,"none")==0 ){ + iLimit = 0; + mxTime = 0; + }else if( sqlite3_strglob("*[0-9]s", zLimit)==0 ){ + iLimit = 0; + mxTime = + (sqlite3_int64)(current_time_in_milliseconds() + 1000.0*atof(zLimit)); + }else{ + iLimit = atoi(zLimit); + if( iLimit<=0 ) iLimit = 30; + mxTime = 0; + } + }else{ + /* Default limit is as much as we can do in 1.000 seconds */ + iLimit = 0; + mxTime = current_time_in_milliseconds()+1000; + } + db_begin_transaction(); + + /* Get the artifact ID for the check-in being analyzed */ + if( zRevision ){ + cid = name_to_typed_rid(zRevision, "ci"); + }else{ + db_must_be_within_tree(); + cid = db_lget_int("checkout", 0); + } + origid = zOrigin ? name_to_typed_rid(zOrigin, "ci") : 0; + + /* Compute all direct ancestors of the check-in being analyzed into + ** the "ancestor" table. */ + if( origid ){ + path_shortest_stored_in_ancestor_table(origid, cid); + }else{ + compute_direct_ancestors(cid); + } + + /* Get filename ID */ + fnid = db_int(0, "SELECT fnid FROM filename WHERE name=%Q", zFilename); + if( fnid==0 ){ + fossil_fatal("no such file: %Q", zFilename); + } + + db_prepare(&q, + "SELECT DISTINCT" + " (SELECT uuid FROM blob WHERE rid=mlink.fid)," + " (SELECT uuid FROM blob WHERE rid=mlink.mid)," + " date(event.mtime)," + " coalesce(event.euser,event.user)," + " mlink.fid" + " FROM mlink, event, ancestor" + " WHERE mlink.fnid=%d" + " AND ancestor.rid=mlink.mid" + " AND event.objid=mlink.mid" + " AND mlink.mid!=mlink.pid" + " ORDER BY ancestor.generation;", + fnid + ); + + while( db_step(&q)==SQLITE_ROW ){ + if( cnt>=3 ){ /* Process at least 3 rows before imposing limits */ + if( (iLimit>0 && cnt>=iLimit) + || (cnt>0 && mxTime>0 && current_time_in_milliseconds()>mxTime) + ){ + p->bMoreToDo = 1; + break; + } + } + rid = db_column_int(&q, 4); + if( cnt==0 ){ + if( !content_get(rid, &toAnnotate) ){ + fossil_fatal("unable to retrieve content of artifact #%d", rid); + } + blob_to_utf8_no_bom(&toAnnotate, 0); + annotation_start(p, &toAnnotate, annFlags); + p->bMoreToDo = origid!=0; + p->origId = origid; + p->showId = cid; + } + p->aVers = fossil_realloc(p->aVers, (p->nVers+1)*sizeof(p->aVers[0])); + p->aVers[p->nVers].zFUuid = fossil_strdup(db_column_text(&q, 0)); + p->aVers[p->nVers].zMUuid = fossil_strdup(db_column_text(&q, 1)); + p->aVers[p->nVers].zDate = fossil_strdup(db_column_text(&q, 2)); + p->aVers[p->nVers].zUser = fossil_strdup(db_column_text(&q, 3)); + if( cnt>0 ){ + content_get(rid, &step); + blob_to_utf8_no_bom(&step, 0); + annotation_step(p, &step, p->nVers-1, annFlags); + blob_reset(&step); + } + p->nVers++; + cnt++; + } + + if( p->nVers==0 ){ + if( zRevision ){ + fossil_fatal("file %s does not exist in check-in %s", + zFilename, zRevision); + }else{ + fossil_fatal("no history for file: %s", zFilename); + } + } + + db_finalize(&q); + db_end_transaction(0); +} + +/* +** Return a color from a gradient. +*/ +unsigned gradient_color(unsigned c1, unsigned c2, int n, int i){ + unsigned c; /* Result color */ + unsigned x1, x2; + if( i==0 || n==0 ) return c1; + else if(i>=n) return c2; + x1 = (c1>>16)&0xff; + x2 = (c2>>16)&0xff; + c = (x1*(n-i) + x2*i)/n<<16 & 0xff0000; + x1 = (c1>>8)&0xff; + x2 = (c2>>8)&0xff; + c |= (x1*(n-i) + x2*i)/n<<8 & 0xff00; + x1 = c1&0xff; + x2 = c2&0xff; + c |= (x1*(n-i) + x2*i)/n & 0xff; + return c; +} + +/* +** WEBPAGE: annotate +** WEBPAGE: blame +** WEBPAGE: praise +** +** URL: /annotate?checkin=ID&filename=FILENAME +** URL: /blame?checkin=ID&filename=FILENAME +** URL: /praise?checkin=ID&filename=FILENAME +** +** Show the most recent change to each line of a text file. /annotate shows +** the date of the changes and the check-in hash (with a link to the +** check-in). /blame and /praise also show the user who made the check-in. +** +** Reverse Annotations: Normally, these web pages look at versions of +** FILENAME moving backwards in time back toward the root check-in. However, +** if the origin= query parameter is used to specify some future check-in +** (example: "origin=trunk") then these pages show changes moving towards +** that alternative origin. Thus using "origin=trunk" on an historical +** version of the file shows the first time each line in the file was changed +** or removed by any subsequent check-in. +** +** Query parameters: +** +** checkin=ID The check-in at which to start the annotation +** filename=FILENAME The filename. +** filevers=BOOLEAN Show file versions rather than check-in versions +** limit=LIMIT Limit the amount of analysis. LIMIT can be one of: +** none No limit +** Xs As much as can be computed in X seconds +** N N versions +** log=BOOLEAN Show a log of versions analyzed +** origin=ID The origin check-in. If unspecified, the root +** check-in over the entire repository is used. +** Specify "origin=trunk" or similar for a reverse +** annotation +** w=BOOLEAN Ignore whitespace +*/ +void annotation_page(void){ + int i; + const char *zLimit; /* Depth limit */ + u64 annFlags = DIFF_STRIP_EOLCR; + int showLog; /* True to display the log */ + int fileVers; /* Show file version instead of check-in versions */ + int ignoreWs; /* Ignore whitespace */ + const char *zFilename; /* Name of file to annotate */ + const char *zRevision; /* Name of check-in from which to start annotation */ + const char *zCI; /* The check-in containing zFilename */ + const char *zOrigin; /* The origin of the analysis */ + int szHash; /* Number of characters in %S display */ + char *zLink; + Annotator ann; + HQuery url; + struct AnnVers *p; + unsigned clr1, clr2, clr; + int bBlame = g.zPath[0]!='a';/* True for BLAME output. False for ANNOTATE. */ + + /* Gather query parameters */ + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + if( robot_restrict("annotate") ) return; + fossil_nice_default(); + zFilename = P("filename"); + zRevision = PD("checkin",0); + zOrigin = P("origin"); + zLimit = P("limit"); + showLog = PB("log"); + fileVers = PB("filevers"); + ignoreWs = PB("w"); + if( ignoreWs ) annFlags |= DIFF_IGNORE_ALLWS; + cgi_check_for_malice(); + + /* compute the annotation */ + annotate_file(&ann, zFilename, zRevision, zLimit, zOrigin, annFlags); + zCI = ann.aVers[0].zMUuid; + + /* generate the web page */ + style_set_current_feature("annotate"); + style_header("Annotation For %h", zFilename); + if( bBlame ){ + url_initialize(&url, "blame"); + }else{ + url_initialize(&url, "annotate"); + } + url_add_parameter(&url, "checkin", P("checkin")); + url_add_parameter(&url, "filename", zFilename); + if( zLimit ){ + url_add_parameter(&url, "limit", zLimit); + } + url_add_parameter(&url, "w", ignoreWs ? "1" : "0"); + url_add_parameter(&url, "log", showLog ? "1" : "0"); + url_add_parameter(&url, "filevers", fileVers ? "1" : "0"); + style_submenu_checkbox("w", "Ignore Whitespace", 0, 0); + style_submenu_checkbox("log", "Log", 0, "toggle_annotation_log"); + style_submenu_checkbox("filevers", "Link to Files", 0, 0); + if( ann.bMoreToDo ){ + style_submenu_element("All Ancestors", "%s", + url_render(&url, "limit", "none", 0, 0)); + } + if( skin_detail_boolean("white-foreground") ){ + clr1 = 0xa04040; + clr2 = 0x4059a0; + }else{ + clr1 = 0xffb5b5; /* Recent changes: red (hot) */ + clr2 = 0xb5e0ff; /* Older changes: blue (cold) */ + } + for(p=ann.aVers, i=0; i<ann.nVers; i++, p++){ + clr = gradient_color(clr1, clr2, ann.nVers-1, i); + ann.aVers[i].zBgColor = mprintf("#%06x", clr); + } + + @ <div id="annotation_log" style='display:%s(showLog?"block":"none");'> + if( zOrigin ){ + zLink = href("%R/finfo?name=%t&from=%!S&to=%!S",zFilename,zCI,zOrigin); + }else{ + zLink = href("%R/finfo?name=%t&from=%!S",zFilename,zCI); + } + @ <h2>Versions of %z(zLink)%h(zFilename)</a> analyzed:</h2> + @ <ol> + for(p=ann.aVers, i=0; i<ann.nVers; i++, p++){ + @ <li><span style='background-color:%s(p->zBgColor);'>%s(p->zDate) + @ check-in %z(href("%R/info/%!S",p->zMUuid))%S(p->zMUuid)</a> + @ artifact %z(href("%R/artifact/%!S",p->zFUuid))%S(p->zFUuid)</a> + @ </span> + } + @ </ol> + @ <hr> + @ </div> + + if( !ann.bMoreToDo ){ + assert( ann.origId==0 ); /* bMoreToDo always set for a point-to-point */ + @ <h2>Origin for each line in + @ %z(href("%R/finfo?name=%h&from=%!S", zFilename, zCI))%h(zFilename)</a> + @ from check-in %z(href("%R/info/%!S",zCI))%S(zCI)</a>:</h2> + }else if( ann.origId>0 ){ + @ <h2>Lines of + @ %z(href("%R/finfo?name=%h&from=%!S", zFilename, zCI))%h(zFilename)</a> + @ from check-in %z(href("%R/info/%!S",zCI))%S(zCI)</a> + @ that are changed by the sequence of edits moving toward + @ check-in %z(href("%R/info/%!S",zOrigin))%S(zOrigin)</a>:</h2> + }else{ + @ <h2>Lines added by the %d(ann.nVers) most recent ancestors of + @ %z(href("%R/finfo?name=%h&from=%!S", zFilename, zCI))%h(zFilename)</a> + @ from check-in %z(href("%R/info/%!S",zCI))%S(zCI)</a>:</h2> + } + @ <pre> + szHash = 10; + for(i=0; i<ann.nOrig; i++){ + int iVers = ann.aOrig[i].iVers; + char *z = (char*)ann.aOrig[i].z; + int n = ann.aOrig[i].n; + char zPrefix[300]; + z[n] = 0; + if( iVers<0 && !ann.bMoreToDo ) iVers = ann.nVers-1; + + if( bBlame ){ + if( iVers>=0 ){ + struct AnnVers *p = ann.aVers+iVers; + const char *zUuid = fileVers ? p->zFUuid : p->zMUuid; + char *zLink = xhref("target='infowindow'", "%R/info/%!S", zUuid); + sqlite3_snprintf(sizeof(zPrefix), zPrefix, + "<span style='background-color:%s'>" + "%s%.10s</a> %s</span> %13.13s:", + p->zBgColor, zLink, zUuid, p->zDate, p->zUser); + fossil_free(zLink); + }else{ + sqlite3_snprintf(sizeof(zPrefix), zPrefix, "%*s", szHash+26, ""); + } + }else{ + if( iVers>=0 ){ + struct AnnVers *p = ann.aVers+iVers; + const char *zUuid = fileVers ? p->zFUuid : p->zMUuid; + char *zLink = xhref("target='infowindow'", "%R/info/%!S", zUuid); + sqlite3_snprintf(sizeof(zPrefix), zPrefix, + "<span style='background-color:%s'>" + "%s%.10s</a> %s</span> %4d:", + p->zBgColor, zLink, zUuid, p->zDate, i+1); + fossil_free(zLink); + }else{ + sqlite3_snprintf(sizeof(zPrefix), zPrefix, "%*s%4d:",szHash+12,"",i+1); + } + } + @ %s(zPrefix) %h(z) + + } + @ </pre> + style_finish_page(); +} + +/* +** COMMAND: annotate +** COMMAND: blame +** COMMAND: praise* +** +** Usage: %fossil annotate|blame|praise ?OPTIONS? FILENAME +** +** Output the text of a file with markings to show when each line of the file +** was last modified. The version currently checked out is shown by default. +** Other versions may be specified using the -r option. The "annotate" command +** shows line numbers and omits the username. The "blame" and "praise" commands +** show the user who made each check-in. +** +** Reverse Annotations: Normally, these commands look at versions of +** FILENAME moving backwards in time back toward the root check-in, and +** thus the output shows the most recent change to each line. However, +** if the -o|--origin option is used to specify some future check-in +** (example: "-o trunk") then these commands show changes moving towards +** that alternative origin. Thus using "-o trunk" on an historical version +** of the file shows the first time each line in the file was changed or +** removed by any subsequent check-in. +** +** With -t or -T, the "blame" and "praise" commands show for each file the +** latest (relative to the revision given by -r) check-in that modified it and +** the check-in's author. If not given, the revision defaults to "current" for +** a check-out. Option -T additionally shows a comment snippet for the check-in. +** +** Options: +** --filevers Show file version numbers rather than +** check-in versions +** -r|--revision VERSION The specific check-in containing the file +** -l|--log List all versions analyzed +** -n|--limit LIMIT LIMIT can be one of: +** N Up to N versions +** Xs As much as possible in X seconds +** none No limit +** -o|--origin VERSION The origin check-in. By default this is the +** root of the repository. Set to the name of +** the main branch (usually "trunk") or +** similar for a reverse annotation. +** -t Show latest check-in and its author for each +** tracked file in the tree as of VERSION +** -T Like -t, plus comment snippet +** -w|--ignore-all-space Ignore white space when comparing lines +** -Z|--ignore-trailing-space Ignore whitespace at line end +** +** See also: [[info]], [[finfo]], [[timeline]] +*/ +void annotate_cmd(void){ + const char *zRevision; /* Revision name, or NULL for current check-in */ + Annotator ann; /* The annotation of the file */ + int i; /* Loop counter */ + const char *zLimit; /* The value to the -n|--limit option */ + const char *zOrig; /* The value for -o|--origin */ + int showLog; /* True to show the log */ + int fileVers; /* Show file version instead of check-in versions */ + u64 annFlags = 0; /* Flags to control annotation properties */ + int bBlame = 0; /* True for BLAME output. False for ANNOTATE. */ + int bTreeInfo = 0; /* Show for the entire tree: 1=checkin, 2=with comment */ + int szHash; /* Display size of a version hash */ + Blob treename; /* Name of file to be annotated */ + char *zFilename; /* Name of file to be annotated */ + + bBlame = g.argv[1][0]!='a'; + if( find_option("t","t",0)!=0 ) bTreeInfo = 1; + if( find_option("T","T",0)!=0 ) bTreeInfo = 2; + zRevision = find_option("revision","r",1); + if( bBlame && bTreeInfo ){ + if( find_repository_option()!=0 && zRevision==0 ){ + fossil_fatal("the -r is required in addition to -R"); + } + db_find_and_open_repository(0, 0); + if( zRevision==0 ) zRevision = "current"; + ls_cmd_rev(zRevision,1,1,0,1,bTreeInfo,0,0); + return; + } + zLimit = find_option("limit","n",1); + zOrig = find_option("origin","o",1); + showLog = find_option("log","l",0)!=0; + if( find_option("ignore-trailing-space","Z",0)!=0 ){ + annFlags = DIFF_IGNORE_EOLWS; + } + if( find_option("ignore-all-space","w",0)!=0 ){ + annFlags = DIFF_IGNORE_ALLWS; /* stronger than DIFF_IGNORE_EOLWS */ + } + fileVers = find_option("filevers",0,0)!=0; + db_must_be_within_tree(); + + /* We should be done with options.. */ + verify_all_options(); + + if( g.argc<3 ) { + usage("FILENAME"); + } + + annFlags |= DIFF_STRIP_EOLCR; + file_tree_name(g.argv[2], &treename, 0, 1); + zFilename = blob_str(&treename); + annotate_file(&ann, zFilename, zRevision, zLimit, zOrig, annFlags); + if( showLog ){ + struct AnnVers *p; + for(p=ann.aVers, i=0; i<ann.nVers; i++, p++){ + fossil_print("version %3d: %s %S file %S\n", + i+1, p->zDate, p->zMUuid, p->zFUuid); + } + fossil_print("---------------------------------------------------\n"); + } + szHash = length_of_S_display(); + for(i=0; i<ann.nOrig; i++){ + int iVers = ann.aOrig[i].iVers; + char *z = (char*)ann.aOrig[i].z; + int n = ann.aOrig[i].n; + struct AnnVers *p; + if( iVers<0 && !ann.bMoreToDo ) iVers = ann.nVers-1; + if( bBlame ){ + if( iVers>=0 ){ + p = ann.aVers + iVers; + fossil_print("%S %s %13.13s: %.*s\n", + fileVers ? p->zFUuid : p->zMUuid, p->zDate, p->zUser, n, z); + }else{ + fossil_print("%*s %.*s\n", szHash+26, "", n, z); + } + }else{ + if( iVers>=0 ){ + p = ann.aVers + iVers; + fossil_print("%S %s %5d: %.*s\n", + fileVers ? p->zFUuid : p->zMUuid, p->zDate, i+1, n, z); + }else{ + fossil_print("%*s %5d: %.*s\n", szHash+11, "", i+1, n, z); + } + } + } +} diff --git a/src/diff.js b/src/diff.js new file mode 100644 index 0000000..b0d09da --- /dev/null +++ b/src/diff.js @@ -0,0 +1,73 @@ +/* Refinements to the display of unified and side-by-side diffs. +** +** In all cases, the table columns tagged with "difftxt" are expanded, +** where possible, to fill the width of the screen. +** +** For a side-by-side diff, if either column is two wide to fit on the +** display, scrollbars are added. The scrollbars are linked, so that +** both sides scroll together. Left and right arrows also scroll. +*/ +window.addEventListener('load',function(){ + var SCROLL_LEN = 25; + function initDiff(diff){ + var txtCols = diff.querySelectorAll('td.difftxt'); + var txtPres = diff.querySelectorAll('td.difftxt pre'); + var width = 0; + if(txtPres.length>=2){ + width = Math.max(txtPres[0].scrollWidth, txtPres[1].scrollWidth); + } + var i; + for(i=0; i<txtCols.length; i++){ + txtCols[i].style.width = width + 'px'; + txtPres[i].style.maxWidth = width + 'px'; + txtPres[i].style.width = width + 'px'; + txtPres[i].onscroll = function(e){ + for(var j=0; j<txtPres.length; j++) txtPres[j].scrollLeft = this.scrollLeft; + }; + } + diff.tabIndex = 0; + diff.onkeydown = function(e){ + e = e || event; + var len = {37: -SCROLL_LEN, 39: SCROLL_LEN}[e.keyCode]; + if( !len ) return; + txtPres[0].scrollLeft += len; + return false; + }; + } + var i, diffs = document.querySelectorAll('table.splitdiff') + for(i=0; i<diffs.length; i++){ + initDiff(diffs[i]); + } + const checkWidth = function f(){ + if(undefined === f.lastWidth){ + f.lastWidth = 0; + } + if( document.body.clientWidth===f.lastWidth ) return; + f.lastWidth = document.body.clientWidth; + var w = f.lastWidth*0.5 - 100; + if(!f.colsL){ + f.colsL = document.querySelectorAll('td.difftxtl pre'); + } + for(let i=0; i<f.colsL.length; i++){ + f.colsL[i].style.width = w + "px"; + f.colsL[i].style.maxWidth = w + "px"; + } + if(!f.colsR){ + f.colsR = document.querySelectorAll('td.difftxtr pre'); + } + for(let i=0; i<f.colsR.length; i++){ + f.colsR[i].style.width = w + "px"; + f.colsR[i].style.maxWidth = w + "px"; + } + if(!f.allDiffs){ + f.allDiffs = document.querySelectorAll('table.diff'); + } + w = f.lastWidth; + for(let i=0; i<f.allDiffs.length; i++){ + f.allDiffs[i].style.width = '100%'; // setting to w causes unsightly horiz. scrollbar + f.allDiffs[i].style.maxWidth = w + "px"; + } + }; + checkWidth(); + window.addEventListener('resize', checkWidth); +}, false); diff --git a/src/diff.tcl b/src/diff.tcl new file mode 100644 index 0000000..f93d2b2 --- /dev/null +++ b/src/diff.tcl @@ -0,0 +1,707 @@ +# Various diff commands ("fossil diff", "fossil stash diff", "fossil fdiff", +# "fossil patch diff", and others) that include the --tk option, or that use "gdiff" +# instead of diff, work by running a Tcl/Tk script which is just this script prepended +# with some variable initializations. The key variable is fossilcmd which is +# usually set something like this: +# +# set fossilcmd {| "./fossil" diff --tcl -i -v} +# +# If there are additional command-line options or arguments, those will be appended +# as well. This variable becomes the argument to "open" to gather the diff +# information that is to be displayed. +# +# Other variables initializations that might be prepended to this script include: +# +# set debug 1 +# set darkmode 1 +# set remotehost hostname +# set remotedir directory-on-hostname +# +# After prepending those "set" commands, the resulting script is run using "tclsh". +# +# This header comment is stripped off by the "mkbuiltin.c" program. +# +set prog { +package require Tk + +array set CFG_light { + TITLE {Fossil Diff} + LN_COL_BG #dddddd + LN_COL_FG #444444 + TXT_COL_BG #ffffff + TXT_COL_FG #000000 + MKR_COL_BG #444444 + MKR_COL_FG #dddddd + CHNG_BG #d0d0ff + ADD_BG #c0ffc0 + RM_BG #ffc0c0 + HR_FG #444444 + HR_PAD_TOP 4 + HR_PAD_BTM 8 + FN_BG #444444 + FN_FG #ffffff + FN_PAD 5 + ERR_FG #ee0000 + PADX 5 + WIDTH 80 + HEIGHT 45 + LB_HEIGHT 25 +} + +array set CFG_dark { + TITLE {Fossil Diff} + LN_COL_BG #dddddd + LN_COL_FG #444444 + TXT_COL_BG #3f3f3f + TXT_COL_FG #dcdccc + MKR_COL_BG #444444 + MKR_COL_FG #dddddd + CHNG_BG #6a6afc + ADD_BG #57934c + RM_BG #ef6767 + HR_FG #444444 + HR_PAD_TOP 4 + HR_PAD_BTM 8 + FN_BG #5e5e5e + FN_FG #ffffff + FN_PAD 5 + ERR_FG #ee0000 + PADX 5 + WIDTH 80 + HEIGHT 45 + LB_HEIGHT 25 +} + +array set CFG_arr { + 0 CFG_light + 1 CFG_dark +} + +array set CFG [array get $CFG_arr($darkmode)] + +if {![namespace exists ttk]} { + interp alias {} ::ttk::scrollbar {} ::scrollbar + interp alias {} ::ttk::menubutton {} ::menubutton +} + +proc dehtml {x} { + set x [regsub -all {<[^>]*>} $x {}] + return [string map {& & < < > > ' ' " \"} $x] +} + +proc cols {} { + return [list .lnA .txtA .mkr .lnB .txtB] +} + +proc colType {c} { + regexp {[a-z]+} $c type + return $type +} + +proc getLine {difftxt N iivar} { + upvar $iivar ii + if {$ii>=$N} {return -1} + set x [lindex $difftxt $ii] + incr ii + return $x +} + +proc reloadDiff {} { + global fossilcmd difftxt + unset -nocomplain difftxt + set idx [.txtA index @0,0] + readDiffs 1 + update + viewDiff $idx +} + +# Populate the difftxt global variable by running the fossilcmd command +# and capturing the output. This is a reload if $redo is 1 and the initial +# load if $redo is 0. +# +proc fetchRawDiff {redo} { + global fossilcmd remotehost remotedir debug difftxt + if {![info exists remotehost] || $remotehost eq ""} { + if {$debug} { + puts "# [lrange $fossilcmd 1 end]" + flush stdout + } + if {[catch { + set in [open $fossilcmd r] + fconfigure $in -encoding utf-8 + set difftxt [split [read $in] \n] + close $in + } msg]} { + if {$redo} { + tk_messageBox -type ok -title Error -message "Unable to refresh:\n$msg" + return 0 + } else { + puts $msg + exit 1 + } + } + return 0 + } else { + set suffix [lrange $fossilcmd 2 end] + set cmd "| ssh $remotehost {cd $remotedir && fossil $suffix}" + if {$debug} { + puts "# [lrange $cmd 1 end]" + flush stdout + } + if {![catch { + set in [open $cmd r] + fconfigure $in -encoding utf-8 + set difftxt [split [read $in] \n] + close $in + }]} { + return 1 + } + set cmd "| ssh $remotehost {cd $remotedir && \ + PATH=\$HOME/bin:/usr/local/bin:/opt/homebrew/bin:\$PATH fossil $suffix}" + if {$debug} { + puts "# [lrange $cmd 1 end]" + flush stdout + } + if {![catch { + set in [open $cmd r] + fconfigure $in -encoding utf-8 + set difftxt [split [read $in] \n] + close $in + } msg]} { + return 1 + } + if {$redo} { + tk_messageBox -type ok -title Error -message "Unable to refresh:\n$msg" + return 0 + } else { + puts $msg + exit 1 + } + } +} + +proc readDiffs {redo} { + global fossilcmd difftxt debug + if {![info exists difftxt]} { + fetchRawDiff $redo + } + set N [llength $difftxt] + set ii 0 + set nDiffs 0 + set n1 0 + set n2 0 + array set widths {txt 3 ln 3 mkr 1} + if {$redo} { + foreach c [cols] {$c config -state normal} + .lnA delete 1.0 end + .txtA delete 1.0 end + .lnB delete 1.0 end + .txtB delete 1.0 end + .mkr delete 1.0 end + .wfiles.lb delete 0 end + } + + + set fromIndex [lsearch -glob $fossilcmd *-from] + set toIndex [lsearch -glob $fossilcmd *-to] + set branchIndex [lsearch -glob $fossilcmd *-branch] + set checkinIndex [lsearch -glob $fossilcmd *-checkin] + if {[lsearch -glob $fossilcmd *-label]>=0 + || [lsearch {xdiff fdiff merge-info} [lindex $fossilcmd 2]]>=0 + } { + set fA {} + set fB {} + } else { + if {[string match *?--external-baseline* $fossilcmd]} { + set fA {external baseline} + } else { + set fA {base check-in} + } + set fB {current check-out} + if {$fromIndex > -1} { + set fA [lindex $fossilcmd $fromIndex+1] + } + if {$toIndex > -1} { + set fB [lindex $fossilcmd $toIndex+1] + } + if {$branchIndex > -1} { + set fA "branch point"; set fB "leaf of branch '[lindex $fossilcmd $branchIndex+1]'" + } + if {$checkinIndex > -1} { + set fA "primary parent"; set fB [lindex $fossilcmd $checkinIndex+1] + } + } + + while {[set line [getLine $difftxt $N ii]] != -1} { + switch -- [lindex $line 0] { + FILE { + incr nDiffs + foreach wx [list [string length $n1] [string length $n2]] { + if {$wx>$widths(ln)} {set widths(ln) $wx} + } + .lnA insert end \n fn \n - + if {$fA==""} { + .txtA insert end "[lindex $line 1]\n" fn \n - + } else { + .txtA insert end "[lindex $line 1] ($fA)\n" fn \n - + } + .mkr insert end \n fn \n - + .lnB insert end \n fn \n - + if {$fB==""} { + .txtB insert end "[lindex $line 2]\n" fn \n - + } else { + .txtB insert end "[lindex $line 2] ($fB)\n" fn \n - + } + .wfiles.lb insert end [lindex $line 2] + set n1 0 + set n2 0 + } + SKIP { + set n [lindex $line 1] + incr n1 $n + incr n2 $n + .lnA insert end ...\n hrln + .txtA insert end [string repeat . 30]\n hrtxt + .mkr insert end \n hrln + .lnB insert end ...\n hrln + .txtB insert end [string repeat . 30]\n hrtxt + } + COM { + set x [lindex $line 1] + incr n1 + incr n2 + .lnA insert end $n1\n - + .txtA insert end $x\n - + .mkr insert end \n - + .lnB insert end $n2\n - + .txtB insert end $x\n - + } + INS { + set x [lindex $line 1] + incr n2 + .lnA insert end \n - + .txtA insert end \n - + .mkr insert end >\n - + .lnB insert end $n2\n - + .txtB insert end $x add \n - + } + DEL { + set x [lindex $line 1] + incr n1 + .lnA insert end $n1\n - + .txtA insert end $x rm \n - + .mkr insert end <\n - + .lnB insert end \n - + .txtB insert end \n - + } + EDIT { + incr n1 + incr n2 + .lnA insert end $n1\n - + .lnB insert end $n2\n - + .mkr insert end |\n - + set nn [llength $line] + for {set i 1} {$i<$nn} {incr i 3} { + set x [lindex $line $i] + if {$x ne ""} { + .txtA insert end $x - + .txtB insert end $x - + } + if {$i+2<$nn} { + set x1 [lindex $line [expr {$i+1}]] + set x2 [lindex $line [expr {$i+2}]] + if {"$x1" eq ""} { + .txtB insert end $x2 add + } elseif {"$x2" eq ""} { + .txtA insert end $x1 rm + } else { + .txtA insert end $x1 chng + .txtB insert end $x2 chng + } + } + } + .txtA insert end \n - + .txtB insert end \n - + } + "" { + foreach wx [list [string length $n1] [string length $n2]] { + if {$wx>$widths(ln)} {set widths(ln) $wx} + } + } + default { + .lnA insert end \n - + .txtA insert end $line\n err + .mkr insert end \n - + .lnB insert end \n - + .txtB insert end $line\n err + } + } + } + + foreach c [cols] { + set type [colType $c] + if {$type ne "txt"} { + $c config -width $widths($type) + } + $c config -state disabled + } + .wfiles.lb config -height $nDiffs + if {$nDiffs <= [.wfiles.lb cget -height]} { + grid remove .wfiles.sb + } + + return $nDiffs +} + +proc viewDiff {idx} { + .txtA yview $idx + .txtA xview moveto 0 +} + +proc cycleDiffs {{reverse 0}} { + if {$reverse} { + set range [.txtA tag prevrange fn @0,0 1.0] + if {$range eq ""} { + viewDiff {fn.last -1c} + } else { + viewDiff [lindex $range 0] + } + } else { + set range [.txtA tag nextrange fn {@0,0 +1c} end] + if {$range eq "" || [lindex [.txtA yview] 1] == 1} { + viewDiff fn.first + } else { + viewDiff [lindex $range 0] + } + } +} + +proc xvis {col} { + set view [$col xview] + return [expr {[lindex $view 1]-[lindex $view 0]}] +} + +proc scroll-x {args} { + set c .txt[expr {[xvis .txtA] < [xvis .txtB] ? "A" : "B"}] + eval $c xview $args +} + +interp alias {} scroll-y {} .txtA yview + +proc noop {args} {} + +proc enableSync {axis} { + update idletasks + interp alias {} sync-$axis {} + rename _sync-$axis sync-$axis +} + +proc disableSync {axis} { + rename sync-$axis _sync-$axis + interp alias {} sync-$axis {} noop +} + +proc sync-x {col first last} { + disableSync x + $col xview moveto [expr {$first*[xvis $col]/($last-$first)}] + foreach side {A B} { + set sb .sbx$side + set xview [.txt$side xview] + if {[lindex $xview 0] > 0 || [lindex $xview 1] < 1} { + grid $sb + eval $sb set $xview + } else { + grid remove $sb + } + } + enableSync x +} + +proc sync-y {first last} { + disableSync y + foreach c [cols] { + $c yview moveto $first + } + if {$first > 0 || $last < 1} { + grid .sby + .sby set $first $last + } else { + grid remove .sby + } + enableSync y +} + +wm withdraw . +wm title . $CFG(TITLE) +wm iconname . $CFG(TITLE) +# Keystroke bindings for on the top-level window for navigation and +# control also fire when those same keystrokes are pressed in the +# Search entry box. Disable them, to prevent the diff screen from +# disappearing abruptly and unexpectedly when searching for "q". +# +bind . <Control-q> exit +bind . <Control-p> {catch searchPrev; break} +bind . <Control-n> {catch searchNext; break} +bind . <Escape><Escape> exit +bind . <Destroy> {after 0 exit} +bind . <Tab> {cycleDiffs; break} +bind . <<PrevWindow>> {cycleDiffs 1; break} +bind . <Control-f> {searchOnOff; break} +bind . <Control-g> {catch searchNext; break} +bind . <Return> { + event generate .bb.files <1> + event generate .bb.files <ButtonRelease-1> + break +} +foreach {key axis args} { + Up y {scroll -5 units} + k y {scroll -5 units} + Down y {scroll 5 units} + j y {scroll 5 units} + Left x {scroll -5 units} + h x {scroll -5 units} + Right x {scroll 5 units} + l x {scroll 5 units} + Prior y {scroll -1 page} + b y {scroll -1 page} + Next y {scroll 1 page} + space y {scroll 1 page} + Home y {moveto 0} + g y {moveto 0} + End y {moveto 1} +} { + bind . <$key> "scroll-$axis $args; break" + bind . <Shift-$key> continue +} + +frame .bb +::ttk::menubutton .bb.files -text "Files" +if {[tk windowingsystem] eq "win32"} { + ::ttk::style theme use winnative + .bb.files configure -padding {20 1 10 2} +} +toplevel .wfiles +wm withdraw .wfiles +update idletasks +wm transient .wfiles . +wm overrideredirect .wfiles 1 +listbox .wfiles.lb -width 0 -height $CFG(LB_HEIGHT) -activestyle none \ + -yscroll {.wfiles.sb set} +::ttk::scrollbar .wfiles.sb -command {.wfiles.lb yview} +grid .wfiles.lb .wfiles.sb -sticky ns +bind .bb.files <1> { + set x [winfo rootx %W] + set y [expr {[winfo rooty %W]+[winfo height %W]}] + wm geometry .wfiles +$x+$y + wm deiconify .wfiles + focus .wfiles.lb +} +bind .wfiles <FocusOut> {wm withdraw .wfiles} +bind .wfiles <Escape> {focus .} +foreach evt {1 Return} { + bind .wfiles.lb <$evt> { + catch { + set idx [lindex [.txtA tag ranges fn] [expr {[%W curselection]*2}]] + viewDiff $idx + } + focus . + break + } +} +bind .wfiles.lb <Motion> { + %W selection clear 0 end + %W selection set @%x,%y +} + +foreach {side syncCol} {A .txtB B .txtA} { + set ln .ln$side + text $ln + $ln tag config - -justify right + + set txt .txt$side + text $txt -width $CFG(WIDTH) -height $CFG(HEIGHT) -wrap none \ + -xscroll "sync-x $syncCol" + catch {$txt config -tabstyle wordprocessor} ;# Required for Tk>=8.5 + foreach tag {add rm chng} { + $txt tag config $tag -background $CFG([string toupper $tag]_BG) + $txt tag lower $tag + } + $txt tag config fn -background $CFG(FN_BG) -foreground $CFG(FN_FG) \ + -justify center + $txt tag config err -foreground $CFG(ERR_FG) +} +text .mkr + +foreach c [cols] { + set keyPrefix [string toupper [colType $c]]_COL_ + if {[tk windowingsystem] eq "win32"} {$c config -font {courier 9}} + $c config -bg $CFG(${keyPrefix}BG) -fg $CFG(${keyPrefix}FG) -borderwidth 0 \ + -padx $CFG(PADX) -yscroll sync-y + $c tag config hrln -spacing1 $CFG(HR_PAD_TOP) -spacing3 $CFG(HR_PAD_BTM) \ + -foreground $CFG(HR_FG) -justify right + $c tag config hrtxt -spacing1 $CFG(HR_PAD_TOP) -spacing3 $CFG(HR_PAD_BTM) \ + -foreground $CFG(HR_FG) -justify center + $c tag config fn -spacing1 $CFG(FN_PAD) -spacing3 $CFG(FN_PAD) + bindtags $c ". $c Text all" + bind $c <1> {focus %W} +} + +::ttk::scrollbar .sby -command {.txtA yview} -orient vertical +::ttk::scrollbar .sbxA -command {.txtA xview} -orient horizontal +::ttk::scrollbar .sbxB -command {.txtB xview} -orient horizontal +frame .spacer + +if {[readDiffs 0] == 0} { + tk_messageBox -type ok -title $CFG(TITLE) -message "No changes" + exit +} +update idletasks + +proc saveDiff {} { + set fn [tk_getSaveFile] + if {$fn==""} return + set out [open $fn wb] + puts $out "#!/usr/bin/tclsh\n#\n# Run this script using 'tclsh' or 'wish'" + puts $out "# to see the graphical diff.\n#" + puts $out "set fossilcmd {}" + puts $out "set darkmode $::darkmode" + puts $out "set debug $::debug" + puts $out "set prog [list $::prog]" + puts $out "set difftxt \173" + foreach e $::difftxt {puts $out [list $e]} + puts $out "\175" + puts $out "eval \$prog" + close $out +} +proc invertDiff {} { + global CFG + array set x [grid info .txtA] + if {$x(-column)==1} { + grid config .lnB -column 0 + grid config .txtB -column 1 + .txtB tag config add -background $CFG(RM_BG) + grid config .lnA -column 3 + grid config .txtA -column 4 + .txtA tag config rm -background $CFG(ADD_BG) + .bb.invert config -text Uninvert + } else { + grid config .lnA -column 0 + grid config .txtA -column 1 + .txtA tag config rm -background $CFG(RM_BG) + grid config .lnB -column 3 + grid config .txtB -column 4 + .txtB tag config add -background $CFG(ADD_BG) + .bb.invert config -text Invert + } + .mkr config -state normal + set clt [.mkr search -all < 1.0 end] + set cgt [.mkr search -all > 1.0 end] + foreach c $clt {.mkr replace $c "$c +1 chars" >} + foreach c $cgt {.mkr replace $c "$c +1 chars" <} + .mkr config -state disabled +} +proc searchOnOff {} { + if {[info exists ::search]} { + unset ::search + .txtA tag remove search 1.0 end + .txtB tag remove search 1.0 end + pack forget .bb.sframe + focus . + } else { + set ::search .txtA + if {![winfo exists .bb.sframe]} { + frame .bb.sframe + ::ttk::entry .bb.sframe.e -width 10 + pack .bb.sframe.e -side left -fill y -expand 1 + bind .bb.sframe.e <Return> {searchNext; break} + ::ttk::button .bb.sframe.nx -text \u2193 -width 1 -command searchNext + ::ttk::button .bb.sframe.pv -text \u2191 -width 1 -command searchPrev + tk_optionMenu .bb.sframe.typ ::search_type \ + Exact {No Case} {RegExp} {Whole Word} + .bb.sframe.typ config -width 10 + set ::search_type Exact + pack .bb.sframe.nx .bb.sframe.pv .bb.sframe.typ -side left + } + pack .bb.sframe -side left + after idle {focus .bb.sframe.e} + } +} +proc searchNext {} {searchStep -forwards +1 1.0 end} +proc searchPrev {} {searchStep -backwards -1 end 1.0} +proc searchStep {direction incr start stop} { + set pattern [.bb.sframe.e get] + if {$pattern==""} return + set count 0 + set w $::search + if {"$w"==".txtA"} {set other .txtB} {set other .txtA} + if {[lsearch [$w mark names] search]<0} { + $w mark set search $start + } + switch $::search_type { + Exact {set st -exact} + {No Case} {set st -nocase} + {RegExp} {set st -regexp} + {Whole Word} {set st -regexp; set pattern \\y$pattern\\y} + } + set idx [$w search -count count $direction $st -- \ + $pattern "search $incr chars" $stop] + if {"$idx"==""} { + set idx [$other search -count count $direction $st -- $pattern $start $stop] + if {"$idx"!=""} { + set this $w + set w $other + set other $this + } else { + set idx [$w search -count count $direction $st -- $pattern $start $stop] + } + } + $w tag remove search 1.0 end + $w mark unset search + $other tag remove search 1.0 end + $other mark unset search + if {"$idx"!=""} { + $w mark set search $idx + $w yview -pickplace $idx + $w tag add search search "$idx +$count chars" + $w tag config search -background {#fcc000} + } + set ::search $w +} +::ttk::button .bb.quit -text {Quit} -command exit +::ttk::button .bb.reload -text {Reload} -command reloadDiff +::ttk::button .bb.invert -text {Invert} -command invertDiff +::ttk::button .bb.save -text {Save As...} -command saveDiff +::ttk::button .bb.search -text {Search} -command searchOnOff +pack .bb.quit -side left +if {$fossilcmd ne ""} { + pack .bb.reload -side left +} +pack .bb.invert -side left +if {$fossilcmd!=""} {pack .bb.save -side left} +pack .bb.files .bb.search -side left +grid rowconfigure . 1 -weight 1 +grid columnconfigure . 1 -weight 1 +grid columnconfigure . 4 -weight 1 +grid .bb -row 0 -columnspan 6 +eval grid [cols] -row 1 -sticky nsew +grid .sby -row 1 -column 5 -sticky ns +grid .sbxA -row 2 -columnspan 2 -sticky ew +grid .spacer -row 2 -column 2 +grid .sbxB -row 2 -column 3 -columnspan 2 -sticky ew + +.spacer config -height [winfo height .sbxA] + +set h [winfo screenheight .] +if {$h > 1280} { + set w [winfo screenwidth .]; # [winfo width] == 32 + if {$w > 2048} { + set w 2048 + } + set g [expr {$w / 3 * 2}]x[expr {$h / 4 * 3}] + wm geometry . $g +} + +wm deiconify . + +} +eval $prog diff --git a/src/diffcmd.c b/src/diffcmd.c new file mode 100644 index 0000000..ce295e2 --- /dev/null +++ b/src/diffcmd.c @@ -0,0 +1,1595 @@ +/* +** Copyright (c) 2007 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) + +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file contains code used to implement the "diff" command +*/ +#include "config.h" +#include "diffcmd.h" +#include <assert.h> + +/* includes needed to catch interrupts */ +#ifdef _WIN32 +# include <windows.h> +#else +# include <signal.h> +#endif + +/* +** Use the right null device for the platform. +*/ +#if defined(_WIN32) +# define NULL_DEVICE "NUL" +#else +# define NULL_DEVICE "/dev/null" +#endif + +/* +** Used when the name for the diff is unknown. +*/ +#define DIFF_NO_NAME "(unknown)" + +/* +** Use the "exec-rel-paths" setting and the --exec-abs-paths and +** --exec-rel-paths command line options to determine whether +** certain external commands are executed using relative paths. +*/ +static int determine_exec_relative_option(int force){ + static int relativePaths = -1; + if( force || relativePaths==-1 ){ + int relPathOption = find_option("exec-rel-paths", 0, 0)!=0; + int absPathOption = find_option("exec-abs-paths", 0, 0)!=0; +#if defined(FOSSIL_ENABLE_EXEC_REL_PATHS) + relativePaths = db_get_boolean("exec-rel-paths", 1); +#else + relativePaths = db_get_boolean("exec-rel-paths", 0); +#endif + if( relPathOption ){ relativePaths = 1; } + if( absPathOption ){ relativePaths = 0; } + } + return relativePaths; +} + +#if INTERFACE +/* +** An array of FileDirList objects describe the files and directories listed +** on the command line of a "diff" command. Only those objects listed are +** actually diffed. +*/ +struct FileDirList { + int nUsed; /* Number of times each entry is used */ + int nName; /* Length of the entry */ + char *zName; /* Text of the entry */ +}; +#endif + +/* +** Return true if zFile is a file named on the azInclude[] list or is +** a file in a directory named on the azInclude[] list. +** +** if azInclude is NULL, then always include zFile. +*/ +static int file_dir_match(FileDirList *p, const char *zFile){ + if( p==0 || strcmp(p->zName,".")==0 ) return 1; + if( filenames_are_case_sensitive() ){ + while( p->zName ){ + if( strcmp(zFile, p->zName)==0 + || (strncmp(zFile, p->zName, p->nName)==0 + && zFile[p->nName]=='/') + ){ + break; + } + p++; + } + }else{ + while( p->zName ){ + if( fossil_stricmp(zFile, p->zName)==0 + || (fossil_strnicmp(zFile, p->zName, p->nName)==0 + && zFile[p->nName]=='/') + ){ + break; + } + p++; + } + } + if( p->zName ){ + p->nUsed++; + return 1; + } + return 0; +} + +/* +** Print details about the compared versions - possibly the working directory +** or the undo buffer. For check-ins, show hash and commit time. +** +** This is intended primarily to go into the "header garbage" that is ignored +** by patch(1). +** +** zFrom and zTo are interpreted as symbolic version names, unless they +** start with '(', in which case they are printed directly. +*/ +void diff_print_versions(const char *zFrom, const char *zTo, DiffConfig *pCfg){ + if( (pCfg->diffFlags & (DIFF_SIDEBYSIDE|DIFF_BRIEF|DIFF_NUMSTAT| + DIFF_HTML|DIFF_WEBPAGE|DIFF_BROWSER|DIFF_JSON|DIFF_TCL))==0 ){ + fossil_print("Fossil-Diff-From: %s\n", + zFrom[0]=='(' ? zFrom : mprintf("%S %s", + rid_to_uuid(symbolic_name_to_rid(zFrom, "ci")), + db_text("","SELECT datetime(%f)||' UTC'", + symbolic_name_to_mtime(zFrom, 0, 0)))); + fossil_print("Fossil-Diff-To: %s\n", + zTo[0]=='(' ? zTo : mprintf("%S %s", + rid_to_uuid(symbolic_name_to_rid(zTo, "ci")), + db_text("","SELECT datetime(%f)||' UTC'", + symbolic_name_to_mtime(zTo, 0, 1)))); + fossil_print("%.66c\n", '-'); + } +} + +/* +** Print the "Index:" message that patches wants to see at the top of a diff. +*/ +void diff_print_index(const char *zFile, DiffConfig *pCfg, Blob *pOut){ + if( (pCfg->diffFlags & + (DIFF_SIDEBYSIDE|DIFF_BRIEF|DIFF_NUMSTAT|DIFF_JSON| + DIFF_WEBPAGE|DIFF_TCL))==0 + ){ + blob_appendf(pOut, "Index: %s\n%.66c\n", zFile, '='); + } +} + +/* +** Print the +++/--- filename lines or whatever filename information +** is appropriate for the output format. +** +*/ +void diff_print_filenames( + const char *zLeft, /* Name of the left file */ + const char *zRight, /* Name of the right file */ + DiffConfig *pCfg, /* Diff configuration */ + Blob *pOut /* Write to this blob, or stdout of this is NULL */ +){ + u64 diffFlags = pCfg->diffFlags; + /* Standardize on /dev/null, regardless of platform. */ + if( pCfg->diffFlags & DIFF_FILE_ADDED ) zLeft = "/dev/null"; + if( pCfg->diffFlags & DIFF_FILE_DELETED ) zRight = "/dev/null"; + if( pCfg->azLabel[0] ) zLeft = pCfg->azLabel[0]; + if( pCfg->azLabel[1] ) zRight = pCfg->azLabel[1]; + if( diffFlags & (DIFF_BRIEF|DIFF_RAW) ){ + /* no-op */ + }else if( diffFlags & DIFF_DEBUG ){ + blob_appendf(pOut, "FILE-LEFT %s\nFILE-RIGHT %s\n", zLeft, zRight); + }else if( diffFlags & DIFF_WEBPAGE ){ + if( fossil_strcmp(zLeft,zRight)==0 ){ + blob_appendf(pOut,"<h1>%h</h1>\n", zLeft); + }else{ + blob_appendf(pOut,"<h1>%h ⇆ %h</h1>\n", zLeft, zRight); + } + }else if( diffFlags & (DIFF_TCL|DIFF_JSON) ){ + if( diffFlags & DIFF_TCL ){ + blob_append(pOut, "FILE ", 5); + blob_append_tcl_literal(pOut, zLeft, (int)strlen(zLeft)); + blob_append_char(pOut, ' '); + blob_append_tcl_literal(pOut, zRight, (int)strlen(zRight)); + blob_append_char(pOut, '\n'); + }else{ + if( pOut ) blob_trim(pOut); + blob_append(pOut, (pCfg->nFile==0 ? "[{" : ",\n{"), -1); + pCfg->nFile++; + blob_append(pOut, "\n \"leftname\":", -1); + blob_append_json_literal(pOut, zLeft, (int)strlen(zLeft)); + blob_append(pOut, ",\n \"rightname\":", -1); + blob_append_json_literal(pOut, zRight, (int)strlen(zRight)); + blob_append(pOut, ",\n \"diff\":\n", -1); + } + }else if( diffFlags & DIFF_SIDEBYSIDE ){ + int w = diff_width(pCfg); + int n1 = strlen(zLeft); + int n2 = strlen(zRight); + int x; + if( n1==n2 && fossil_strcmp(zLeft,zRight)==0 ){ + if( n1>w*2 ) n1 = w*2; + x = w*2+17 - (n1+2); + blob_appendf(pOut, "%.*c %.*s %.*c\n", + x/2, '=', n1, zLeft, (x+1)/2, '='); + }else{ + if( w<20 ) w = 20; + if( n1>w-10 ) n1 = w - 10; + if( n2>w-10 ) n2 = w - 10; + blob_appendf(pOut, "%.*c %.*s %.*c versus %.*c %.*s %.*c\n", + (w-n1+10)/2, '=', n1, zLeft, (w-n1+1)/2, '=', + (w-n2)/2, '=', n2, zRight, (w-n2+1)/2, '='); + } + }else{ + blob_appendf(pOut, "--- %s\t\n+++ %s\t\n", zLeft, zRight); + } +} + + +/* +** Default header texts for diff with --webpage +*/ +static const char zWebpageHdr[] = +@ <!DOCTYPE html> +@ <html> +@ <head> +@ <meta charset="UTF-8"> +@ <style> +@ body { +@ background-color: white; +@ } +@ h1 { +@ font-size: 150%; +@ } +@ +@ table.diff { +@ width: 100%; +@ border-spacing: 0; +@ border: 1px solid black; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ table.diff td { +@ vertical-align: top; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ table.diff pre { +@ margin: 0 0 0 0; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffln { +@ width: fit-content; +@ text-align: right; +@ padding: 0 1em 0 0; +@ } +@ td.difflne { +@ padding-bottom: 0.4em; +@ } +@ td.diffsep { +@ width: fit-content; +@ padding: 0 0.3em 0 1em; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffsep pre { +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt pre { +@ overflow-x: auto; +@ } +@ td.diffln ins { +@ background-color: #a0e4b2; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffln del { +@ background-color: #ffc0c0; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt del { +@ background-color: #ffe8e8; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt del > del { +@ background-color: #ffc0c0; +@ text-decoration: none; +@ font-weight: bold; +@ } +@ td.difftxt del > del.edit { +@ background-color: #c0c0ff; +@ text-decoration: none; +@ font-weight: bold; +@ } +@ td.difftxt ins { +@ background-color: #dafbe1; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt ins > ins { +@ background-color: #a0e4b2; +@ text-decoration: none; +@ font-weight: bold; +@ } +@ td.difftxt ins > ins.edit { +@ background-color: #c0c0ff; +@ text-decoration: none; +@ font-weight: bold; +@ } +@ @media (prefers-color-scheme: dark) { +@ body { +@ background-color: #353535; +@ color: #ffffff; +@ } +@ td.diffln ins { +@ background-color: #559855; +@ color: #000000; +@ } +@ td.diffln del { +@ background-color: #cc5555; +@ color: #000000; +@ } +@ td.difftxt del { +@ background-color: #f9cfcf; +@ color: #000000; +@ } +@ td.difftxt del > del { +@ background-color: #cc5555; +@ color: #000000; +@ } +@ td.difftxt ins { +@ background-color: #a2dbb2; +@ color: #000000; +@ } +@ td.difftxt ins > ins { +@ background-color: #559855; +@ } +@ } +@ +@ </style> +@ </head> +@ <body> +; +static const char zWebpageHdrDark[] = +@ <!DOCTYPE html> +@ <html> +@ <head> +@ <meta charset="UTF-8"> +@ <style> +@ body { +@ background-color: #353535; +@ color: #ffffff; +@ } +@ h1 { +@ font-size: 150%; +@ } +@ +@ table.diff { +@ width: 100%; +@ border-spacing: 0; +@ border: 1px solid black; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ table.diff td { +@ vertical-align: top; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ table.diff pre { +@ margin: 0 0 0 0; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffln { +@ width: fit-content; +@ text-align: right; +@ padding: 0 1em 0 0; +@ } +@ td.difflne { +@ padding-bottom: 0.4em; +@ } +@ td.diffsep { +@ width: fit-content; +@ padding: 0 0.3em 0 1em; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffsep pre { +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt pre { +@ overflow-x: auto; +@ } +@ td.diffln ins { +@ background-color: #559855; +@ color: #000000; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.diffln del { +@ background-color: #cc5555; +@ color: #000000; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt del { +@ background-color: #f9cfcf; +@ color: #000000; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt del > del { +@ background-color: #cc5555; +@ color: #000000; +@ text-decoration: none; +@ font-weight: bold; +@ } +@ td.difftxt del > del.edit { +@ background-color: #c0c0ff; +@ text-decoration: none; +@ font-weight: bold; +@ } +@ td.difftxt ins { +@ background-color: #a2dbb2; +@ color: #000000; +@ text-decoration: none; +@ line-height: inherit; +@ font-size: inherit; +@ } +@ td.difftxt ins > ins { +@ background-color: #559855; +@ text-decoration: none; +@ font-weight: bold; +@ } +@ td.difftxt ins > ins.edit { +@ background-color: #c0c0ff; +@ text-decoration: none; +@ font-weight: bold; +@ } +@ +@ </style> +@ </head> +@ <body> +; +const char zWebpageEnd[] = +@ </body> +@ </html> +; + +/* +** State variables used by the --browser option for diff. These must +** be static variables, not elements of DiffConfig, since they are +** used by the interrupt handler. +*/ +static char *tempDiffFilename; /* File holding the diff HTML */ +static FILE *diffOut; /* Open to write into tempDiffFilename */ + +/* Amount of delay (in milliseconds) between launching the +** web browser and deleting the temporary file used by --browser +*/ +#ifndef FOSSIL_BROWSER_DIFF_DELAY +# define FOSSIL_BROWSER_DIFF_DELAY 5000 /* 5 seconds by default */ +#endif + +/* +** If we catch a single while writing the temporary file for the --browser +** diff output, then delete the temporary file and exit. +*/ +static void diff_www_interrupt(int NotUsed){ + (void)NotUsed; + if( diffOut ) fclose(diffOut); + if( tempDiffFilename ) file_delete(tempDiffFilename); + exit(1); +} +#ifdef _WIN32 +static BOOL WINAPI diff_console_ctrl_handler(DWORD dwCtrlType){ + if( dwCtrlType==CTRL_C_EVENT ) diff_www_interrupt(0); + return FALSE; +} +#endif + + +/* +** Do preliminary setup and output before computing a diff. +** +** For --browser, redirect stdout to a temporary file that will +** hold the result. Make arrangements to delete that temporary +** file if the diff is interrupted. +** +** For --browser and --webpage, output the HTML header. +*/ +void diff_begin(DiffConfig *pCfg){ + if( (pCfg->diffFlags & DIFF_BROWSER)!=0 ){ + tempDiffFilename = fossil_temp_filename(); + tempDiffFilename = sqlite3_mprintf("%z.html", tempDiffFilename); + diffOut = fossil_freopen(tempDiffFilename,"wb",stdout); + if( diffOut==0 ){ + fossil_fatal("unable to create temporary file \"%s\"", + tempDiffFilename); + } +#ifndef _WIN32 + signal(SIGINT, diff_www_interrupt); +#else + SetConsoleCtrlHandler(diff_console_ctrl_handler, TRUE); +#endif + } + if( (pCfg->diffFlags & DIFF_WEBPAGE)!=0 ){ + fossil_print("%s",(pCfg->diffFlags & DIFF_DARKMODE)!=0 ? zWebpageHdrDark : + zWebpageHdr); + fflush(stdout); + } +} + +/* Do any final output required by a diff and complete the diff +** process. +** +** For --browser and --webpage, output any javascript required by +** the diff. (Currently JS is only needed for side-by-side diffs). +** +** For --browser, close the connection to the temporary file, then +** launch a web browser to view the file. After a delay +** of FOSSIL_BROWSER_DIFF_DELAY milliseconds, delete the temp file. +*/ +void diff_end(DiffConfig *pCfg, int nErr){ + if( (pCfg->diffFlags & DIFF_WEBPAGE)!=0 ){ + if( pCfg->diffFlags & DIFF_SIDEBYSIDE ){ + const unsigned char *zJs = builtin_file("diff.js", 0); + fossil_print("<script>\n%s</script>\n", zJs); + } + fossil_print("%s", zWebpageEnd); + } + if( (pCfg->diffFlags & DIFF_BROWSER)!=0 && nErr==0 ){ + char *zCmd = mprintf("%s %$", fossil_web_browser(), tempDiffFilename); + fclose(diffOut); + diffOut = fossil_freopen(NULL_DEVICE, "wb", stdout); + fossil_system(zCmd); + fossil_free(zCmd); + diffOut = 0; + sqlite3_sleep(FOSSIL_BROWSER_DIFF_DELAY); + file_delete(tempDiffFilename); + sqlite3_free(tempDiffFilename); + tempDiffFilename = 0; + } + if( (pCfg->diffFlags & DIFF_JSON)!=0 && pCfg->nFile>0 ){ + fossil_print("]\n"); + } +} + +/* +** Show the difference between two files, one in memory and one on disk. +** +** The difference is the set of edits needed to transform pFile1 into +** zFile2. The content of pFile1 is in memory. zFile2 exists on disk. +*/ +void diff_file( + Blob *pFile1, /* In memory content to compare from */ + const char *zFile2, /* On disk content to compare to */ + const char *zName, /* Display name of the file */ + DiffConfig *pCfg, /* Flags to control the diff */ + Blob *pOut /* Blob to store diff output */ +){ + if( pCfg->zDiffCmd==0 ){ + Blob out; /* Diff output text */ + Blob file2; /* Content of zFile2 */ + const char *zName2; /* Name of zFile2 for display */ + + /* Read content of zFile2 into memory */ + blob_zero(&file2); + if( pCfg->diffFlags & DIFF_FILE_DELETED || file_size(zFile2, ExtFILE)<0 ){ + zName2 = NULL_DEVICE; + }else{ + blob_read_from_file(&file2, zFile2, ExtFILE); + zName2 = zName; + } + + /* Compute and output the differences */ + if( (pCfg->diffFlags & DIFF_BRIEF) && !(pCfg->diffFlags & DIFF_NUMSTAT) ){ + if( blob_compare(pFile1, &file2) ){ + fossil_print("CHANGED %s\n", zName); + } + }else{ + blob_zero(&out); + text_diff(pFile1, &file2, &out, pCfg); + if( blob_size(&out) ){ + if( pCfg->diffFlags & DIFF_NUMSTAT ){ + if( !(pCfg->diffFlags & DIFF_BRIEF) ){ + blob_appendf(pOut, "%s %s\n", blob_str(&out), zName); + } + }else{ + diff_print_filenames(zName, zName2, pCfg, pOut); + blob_appendf(pOut, "%s\n", blob_str(&out)); + } + } + blob_reset(&out); + } + + /* Release memory resources */ + blob_reset(&file2); + }else{ + Blob nameFile1; /* Name of temporary file to old pFile1 content */ + Blob cmd; /* Text of command to run */ + int useTempfile = 1; + + if( (pCfg->diffFlags & DIFF_INCBINARY)==0 ){ + Blob file2; + if( looks_like_binary(pFile1) ){ + fossil_print("%s",DIFF_CANNOT_COMPUTE_BINARY); + return; + } + if( pCfg->zBinGlob ){ + Glob *pBinary = glob_create(pCfg->zBinGlob); + if( glob_match(pBinary, zName) ){ + fossil_print("%s",DIFF_CANNOT_COMPUTE_BINARY); + glob_free(pBinary); + return; + } + glob_free(pBinary); + } + blob_zero(&file2); + if( file_size(zFile2, ExtFILE)>=0 ){ + blob_read_from_file(&file2, zFile2, ExtFILE); + } + if( looks_like_binary(&file2) ){ + fossil_print("%s",DIFF_CANNOT_COMPUTE_BINARY); + blob_reset(&file2); + return; + } + blob_reset(&file2); + } + + /* Construct a temporary file to hold pFile1 based on the name of + ** zFile2 */ + file_tempname(&nameFile1, zFile2, "orig"); +#if !defined(_WIN32) + /* On Unix, use /dev/null for added or deleted files. */ + if( pCfg->diffFlags & DIFF_FILE_ADDED ){ + blob_init(&nameFile1, NULL_DEVICE, -1); + useTempfile = 0; + }else if( pCfg->diffFlags & DIFF_FILE_DELETED ){ + zFile2 = NULL_DEVICE; + } +#endif + if( useTempfile ) blob_write_to_file(pFile1, blob_str(&nameFile1)); + + /* Construct the external diff command */ + blob_zero(&cmd); + blob_append(&cmd, pCfg->zDiffCmd, -1); + if( pCfg->diffFlags & DIFF_INVERT ){ + blob_append_escaped_arg(&cmd, zFile2, 1); + blob_append_escaped_arg(&cmd, blob_str(&nameFile1), 1); + }else{ + blob_append_escaped_arg(&cmd, blob_str(&nameFile1), 1); + blob_append_escaped_arg(&cmd, zFile2, 1); + } + + /* Run the external diff command */ + fossil_system(blob_str(&cmd)); + + /* Delete the temporary file and clean up memory used */ + if( useTempfile ) file_delete(blob_str(&nameFile1)); + blob_reset(&nameFile1); + blob_reset(&cmd); + } +} + +/* +** Show the difference between two files, both in memory. +** +** The difference is the set of edits needed to transform pFile1 into +** pFile2. +** +** Use the internal diff logic if zDiffCmd is NULL. Otherwise call the +** command zDiffCmd to do the diffing. +** +** When using an external diff program, zBinGlob contains the GLOB patterns +** for file names to treat as binary. If fIncludeBinary is zero, these files +** will be skipped in addition to files that may contain binary content. +*/ +void diff_file_mem( + Blob *pFile1, /* In memory content to compare from */ + Blob *pFile2, /* In memory content to compare to */ + const char *zName, /* Display name of the file */ + DiffConfig *pCfg /* Diff flags */ +){ + if( (pCfg->diffFlags & DIFF_BRIEF) && !(pCfg->diffFlags & DIFF_NUMSTAT) ){ + return; + } + if( pCfg->zDiffCmd==0 ){ + Blob out; /* Diff output text */ + + blob_zero(&out); + text_diff(pFile1, pFile2, &out, pCfg); + if( pCfg->diffFlags & DIFF_NUMSTAT ){ + if( !(pCfg->diffFlags & DIFF_BRIEF) ){ + fossil_print("%s %s\n", blob_str(&out), zName); + } + }else{ + diff_print_filenames(zName, zName, pCfg, 0); + fossil_print("%s\n", blob_str(&out)); + } + + /* Release memory resources */ + blob_reset(&out); + }else{ + Blob cmd; + Blob temp1; + Blob temp2; + int useTempfile1 = 1; + int useTempfile2 = 1; + + if( (pCfg->diffFlags & DIFF_INCBINARY)==0 ){ + if( looks_like_binary(pFile1) || looks_like_binary(pFile2) ){ + fossil_print("%s",DIFF_CANNOT_COMPUTE_BINARY); + return; + } + if( pCfg->zBinGlob ){ + Glob *pBinary = glob_create(pCfg->zBinGlob); + if( glob_match(pBinary, zName) ){ + fossil_print("%s",DIFF_CANNOT_COMPUTE_BINARY); + glob_free(pBinary); + return; + } + glob_free(pBinary); + } + } + + /* Construct temporary file names */ + file_tempname(&temp1, zName, "before"); + file_tempname(&temp2, zName, "after"); +#if !defined(_WIN32) + /* On Unix, use /dev/null for added or deleted files. */ + if( pCfg->diffFlags & DIFF_FILE_ADDED ){ + useTempfile1 = 0; + blob_init(&temp1, NULL_DEVICE, -1); + }else if( pCfg->diffFlags & DIFF_FILE_DELETED ){ + useTempfile2 = 0; + blob_init(&temp2, NULL_DEVICE, -1); + } +#endif + if( useTempfile1 ) blob_write_to_file(pFile1, blob_str(&temp1)); + if( useTempfile2 ) blob_write_to_file(pFile2, blob_str(&temp2)); + + /* Construct the external diff command */ + blob_zero(&cmd); + blob_append(&cmd, pCfg->zDiffCmd, -1); + blob_append_escaped_arg(&cmd, blob_str(&temp1), 1); + blob_append_escaped_arg(&cmd, blob_str(&temp2), 1); + + /* Run the external diff command */ + fossil_system(blob_str(&cmd)); + + /* Delete the temporary file and clean up memory used */ + if( useTempfile1 ) file_delete(blob_str(&temp1)); + if( useTempfile2 ) file_delete(blob_str(&temp2)); + + blob_reset(&temp1); + blob_reset(&temp2); + blob_reset(&cmd); + } +} + +/* +** Return true if the disk file is identical to the Blob. Return zero +** if the files differ in any way. +*/ +int file_same_as_blob(Blob *blob, const char *zDiskFile){ + Blob file; + int rc = 0; + if( blob_size(blob)!=file_size(zDiskFile, ExtFILE) ) return 0; + blob_zero(&file); + blob_read_from_file(&file, zDiskFile, ExtFILE); + if( blob_size(&file)!=blob_size(blob) ){ + rc = 0; + }else{ + rc = memcmp(blob_buffer(&file), blob_buffer(blob), blob_size(&file))==0; + } + blob_reset(&file); + return rc; +} + +/* +** Run a diff between the version zFrom and files on disk in the current +** working checkout. zFrom might be NULL which means to simply show the +** difference between the edited files on disk and the check-out on which +** they are based. +** +** Use the internal diff logic if zDiffCmd is NULL. Otherwise call the +** command zDiffCmd to do the diffing. +** +** When using an external diff program, zBinGlob contains the GLOB patterns +** for file names to treat as binary. If fIncludeBinary is zero, these files +** will be skipped in addition to files that may contain binary content. +*/ +void diff_version_to_checkout( + const char *zFrom, /* Version to difference from */ + DiffConfig *pCfg, /* Flags controlling diff output */ + FileDirList *pFileDir, /* Which files to diff */ + Blob *pOut /* Blob to output diff instead of stdout */ +){ + int vid; + Blob sql; + Stmt q; + int asNewFile; /* Treat non-existent files as empty files */ + int isNumStat; /* True for --numstat */ + + asNewFile = (pCfg->diffFlags & (DIFF_VERBOSE|DIFF_NUMSTAT|DIFF_HTML))!=0; + isNumStat = (pCfg->diffFlags & (DIFF_NUMSTAT|DIFF_TCL|DIFF_HTML))!=0; + vid = db_lget_int("checkout", 0); + vfile_check_signature(vid, CKSIG_ENOTFILE); + blob_zero(&sql); + db_begin_transaction(); + if( zFrom ){ + int rid = name_to_typed_rid(zFrom, "ci"); + if( !is_a_version(rid) ){ + fossil_fatal("no such check-in: %s", zFrom); + } + load_vfile_from_rid(rid); + blob_append_sql(&sql, + "SELECT v2.pathname, v2.deleted, v2.chnged, v2.rid==0, v1.rid, v1.islink" + " FROM vfile v1, vfile v2 " + " WHERE v1.pathname=v2.pathname AND v1.vid=%d AND v2.vid=%d" + " AND (v2.deleted OR v2.chnged OR v1.mrid!=v2.rid)" + "UNION " + "SELECT pathname, 1, 0, 0, 0, islink" + " FROM vfile v1" + " WHERE v1.vid=%d" + " AND NOT EXISTS(SELECT 1 FROM vfile v2" + " WHERE v2.vid=%d AND v2.pathname=v1.pathname)" + "UNION " + "SELECT pathname, 0, 0, 1, 0, islink" + " FROM vfile v2" + " WHERE v2.vid=%d" + " AND NOT EXISTS(SELECT 1 FROM vfile v1" + " WHERE v1.vid=%d AND v1.pathname=v2.pathname)" + " ORDER BY 1 /*scan*/", + rid, vid, rid, vid, vid, rid + ); + }else{ + blob_append_sql(&sql, + "SELECT pathname, deleted, chnged , rid==0, rid, islink" + " FROM vfile" + " WHERE vid=%d" + " AND (deleted OR chnged OR rid==0)" + " ORDER BY pathname /*scan*/", + vid + ); + } + if( (pCfg->diffFlags & DIFF_SHOW_VERS)!=0 ){ + diff_print_versions(zFrom ? zFrom : db_lget("checkout-hash", 0), + "(workdir)", pCfg); + } + db_prepare(&q, "%s", blob_sql_text(&sql)); + blob_reset(&sql); + while( db_step(&q)==SQLITE_ROW ){ + const char *zPathname = db_column_text(&q,0); + int isDeleted = db_column_int(&q, 1); + int isChnged = db_column_int(&q,2); + int isNew = db_column_int(&q,3); + int srcid = db_column_int(&q, 4); + int isLink = db_column_int(&q, 5); + const char *zFullName; + int showDiff = 1; + Blob fname; + + if( !file_dir_match(pFileDir, zPathname) ) continue; + if( determine_exec_relative_option(0) ){ + blob_zero(&fname); + file_relative_name(zPathname, &fname, 1); + }else{ + blob_set(&fname, g.zLocalRoot); + blob_append(&fname, zPathname, -1); + } + zFullName = blob_str(&fname); + pCfg->diffFlags &= (~DIFF_FILE_MASK); + if( isDeleted ){ + if( !isNumStat ){ fossil_print("DELETED %s\n", zPathname); } + pCfg->diffFlags |= DIFF_FILE_DELETED; + if( !asNewFile ){ showDiff = 0; zFullName = NULL_DEVICE; } + }else if( file_access(zFullName, F_OK) ){ + if( !isNumStat ){ fossil_print("MISSING %s\n", zPathname); } + if( !asNewFile ){ showDiff = 0; } + }else if( isNew ){ + if( !isNumStat ){ fossil_print("ADDED %s\n", zPathname); } + pCfg->diffFlags |= DIFF_FILE_ADDED; + srcid = 0; + if( !asNewFile ){ showDiff = 0; } + }else if( isChnged==3 ){ + if( !isNumStat ){ fossil_print("ADDED_BY_MERGE %s\n", zPathname); } + pCfg->diffFlags |= DIFF_FILE_ADDED; + srcid = 0; + if( !asNewFile ){ showDiff = 0; } + }else if( isChnged==5 ){ + if( !isNumStat ){ fossil_print("ADDED_BY_INTEGRATE %s\n", zPathname); } + pCfg->diffFlags |= DIFF_FILE_ADDED; + srcid = 0; + if( !asNewFile ){ showDiff = 0; } + } + if( showDiff ){ + Blob content; + if( !isLink != !file_islink(zFullName) ){ + diff_print_index(zPathname, pCfg, 0); + diff_print_filenames(zPathname, zPathname, pCfg, 0); + fossil_print("%s",DIFF_CANNOT_COMPUTE_SYMLINK); + continue; + } + if( srcid>0 ){ + content_get(srcid, &content); + }else{ + blob_zero(&content); + } + if( isChnged==0 + || pCfg->diffFlags & DIFF_FILE_DELETED + || !file_same_as_blob(&content, zFullName) + ){ + diff_print_index(zPathname, pCfg, pOut); + diff_file(&content, zFullName, zPathname, pCfg, pOut); + } + blob_reset(&content); + } + blob_reset(&fname); + } + db_finalize(&q); + db_end_transaction(1); /* ROLLBACK */ +} + +/* +** Run a diff from the undo buffer to files on disk. +** +** Use the internal diff logic if zDiffCmd is NULL. Otherwise call the +** command zDiffCmd to do the diffing. +** +** When using an external diff program, zBinGlob contains the GLOB patterns +** for file names to treat as binary. If fIncludeBinary is zero, these files +** will be skipped in addition to files that may contain binary content. +*/ +static void diff_undo_to_checkout( + DiffConfig *pCfg, /* Flags controlling diff output */ + FileDirList *pFileDir /* List of files and directories to diff */ +){ + Stmt q; + Blob content; + db_prepare(&q, "SELECT pathname, content FROM undo"); + blob_init(&content, 0, 0); + if( (pCfg->diffFlags & DIFF_SHOW_VERS)!=0 ){ + diff_print_versions("(undo)", "(workdir)", pCfg); + } + while( db_step(&q)==SQLITE_ROW ){ + char *zFullName; + const char *zFile = (const char*)db_column_text(&q, 0); + if( !file_dir_match(pFileDir, zFile) ) continue; + zFullName = mprintf("%s%s", g.zLocalRoot, zFile); + db_column_blob(&q, 1, &content); + diff_file(&content, zFullName, zFile, pCfg, 0); + fossil_free(zFullName); + blob_reset(&content); + } + db_finalize(&q); +} + +/* +** Show the difference between two files identified by ManifestFile +** entries. +** +** Use the internal diff logic if zDiffCmd is NULL. Otherwise call the +** command zDiffCmd to do the diffing. +** +** When using an external diff program, zBinGlob contains the GLOB patterns +** for file names to treat as binary. If fIncludeBinary is zero, these files +** will be skipped in addition to files that may contain binary content. +*/ +static void diff_manifest_entry( + struct ManifestFile *pFrom, + struct ManifestFile *pTo, + DiffConfig *pCfg +){ + Blob f1, f2; + int rid; + const char *zName; + if( pFrom ){ + zName = pFrom->zName; + }else if( pTo ){ + zName = pTo->zName; + }else{ + zName = DIFF_NO_NAME; + } + if( (pCfg->diffFlags & DIFF_BRIEF) && !(pCfg->diffFlags & DIFF_NUMSTAT) ){ + return; + } + diff_print_index(zName, pCfg, 0); + if( pFrom ){ + rid = uuid_to_rid(pFrom->zUuid, 0); + content_get(rid, &f1); + }else{ + blob_zero(&f1); + } + if( pTo ){ + rid = uuid_to_rid(pTo->zUuid, 0); + content_get(rid, &f2); + }else{ + blob_zero(&f2); + } + diff_file_mem(&f1, &f2, zName, pCfg); + blob_reset(&f1); + blob_reset(&f2); +} + +/* +** Output the differences between two check-ins. +** +** Use the internal diff logic if zDiffCmd is NULL. Otherwise call the +** command zDiffCmd to do the diffing. +** +** When using an external diff program, zBinGlob contains the GLOB patterns +** for file names to treat as binary. If fIncludeBinary is zero, these files +** will be skipped in addition to files that may contain binary content. +*/ +static void diff_two_versions( + const char *zFrom, + const char *zTo, + DiffConfig *pCfg, + FileDirList *pFileDir +){ + Manifest *pFrom, *pTo; + ManifestFile *pFromFile, *pToFile; + int asNewFlag = (pCfg->diffFlags & (DIFF_VERBOSE|DIFF_NUMSTAT))!=0 ? 1 : 0; + + pFrom = manifest_get_by_name(zFrom, 0); + manifest_file_rewind(pFrom); + pFromFile = manifest_file_next(pFrom,0); + pTo = manifest_get_by_name(zTo, 0); + manifest_file_rewind(pTo); + pToFile = manifest_file_next(pTo,0); + if( (pCfg->diffFlags & DIFF_SHOW_VERS)!=0 ){ + diff_print_versions(zFrom, zTo, pCfg); + } + while( pFromFile || pToFile ){ + int cmp; + if( pFromFile==0 ){ + cmp = +1; + }else if( pToFile==0 ){ + cmp = -1; + }else{ + cmp = fossil_strcmp(pFromFile->zName, pToFile->zName); + } + pCfg->diffFlags &= (~DIFF_FILE_MASK); + if( cmp<0 ){ + if( file_dir_match(pFileDir, pFromFile->zName) ){ + if( (pCfg->diffFlags & (DIFF_NUMSTAT|DIFF_HTML))==0 ){ + fossil_print("DELETED %s\n", pFromFile->zName); + } + pCfg->diffFlags |= DIFF_FILE_DELETED; + if( asNewFlag ){ + diff_manifest_entry(pFromFile, 0, pCfg); + } + } + pFromFile = manifest_file_next(pFrom,0); + }else if( cmp>0 ){ + if( file_dir_match(pFileDir, pToFile->zName) ){ + if( (pCfg->diffFlags & + (DIFF_NUMSTAT|DIFF_HTML|DIFF_TCL|DIFF_JSON))==0 ){ + fossil_print("ADDED %s\n", pToFile->zName); + } + pCfg->diffFlags |= DIFF_FILE_ADDED; + if( asNewFlag ){ + diff_manifest_entry(0, pToFile, pCfg); + } + } + pToFile = manifest_file_next(pTo,0); + }else if( fossil_strcmp(pFromFile->zUuid, pToFile->zUuid)==0 ){ + /* No changes */ + (void)file_dir_match(pFileDir, pFromFile->zName); /* Record name usage */ + pFromFile = manifest_file_next(pFrom,0); + pToFile = manifest_file_next(pTo,0); + }else{ + if( file_dir_match(pFileDir, pToFile->zName) ){ + if((pCfg->diffFlags & DIFF_BRIEF) && !(pCfg->diffFlags & DIFF_NUMSTAT)){ + fossil_print("CHANGED %s\n", pFromFile->zName); + }else{ + diff_manifest_entry(pFromFile, pToFile, pCfg); + } + } + pFromFile = manifest_file_next(pFrom,0); + pToFile = manifest_file_next(pTo,0); + } + } + manifest_destroy(pFrom); + manifest_destroy(pTo); +} + +/* +** Compute the difference from an external tree of files to the current +** working checkout with its edits. +** +** To put it another way: Every managed file in the current working +** checkout is compared to the file with same name under zExternBase. The +** zExternBase files are on the left and the files in the current working +** directory are on the right. +*/ +void diff_externbase_to_checkout( + const char *zExternBase, /* Remote tree to use as the baseline */ + DiffConfig *pCfg, /* Diff settings */ + FileDirList *pFileDir /* Only look at these files */ +){ + int vid; + Stmt q; + + vid = db_lget_int("checkout",0); + if( file_isdir(zExternBase, ExtFILE)!=1 ){ + fossil_fatal("\"%s\" is not a directory", zExternBase); + } + db_prepare(&q, + "SELECT pathname FROM vfile WHERE vid=%d ORDER BY pathname", + vid + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zFile; /* Name of file in the repository */ + char *zLhs; /* Full name of left-hand side file */ + char *zRhs; /* Full name of right-hand side file */ + Blob rhs; /* Full text of RHS */ + Blob lhs; /* Full text of LHS */ + + zFile = db_column_text(&q,0); + if( !file_dir_match(pFileDir, zFile) ) continue; + zLhs = mprintf("%s/%s", zExternBase, zFile); + zRhs = mprintf("%s%s", g.zLocalRoot, zFile); + if( file_size(zLhs, ExtFILE)<0 ){ + blob_zero(&lhs); + }else{ + blob_read_from_file(&lhs, zLhs, ExtFILE); + } + blob_read_from_file(&rhs, zRhs, ExtFILE); + if( blob_size(&lhs)!=blob_size(&rhs) + || memcmp(blob_buffer(&lhs), blob_buffer(&rhs), blob_size(&lhs))!=0 + ){ + diff_print_index(zFile, pCfg, 0); + diff_file_mem(&lhs, &rhs, zFile, pCfg); + } + blob_reset(&lhs); + blob_reset(&rhs); + fossil_free(zLhs); + fossil_free(zRhs); + } + db_finalize(&q); +} + + +/* +** Return the name of the external diff command, or return NULL if +** no external diff command is defined. +*/ +const char *diff_command_external(int guiDiff){ + const char *zName; + zName = guiDiff ? "gdiff-command" : "diff-command"; + return db_get(zName, 0); +} + +/* +** Return true if it reasonable to run "diff -tk" for "gdiff". +** +** Details: Return true if all of the following are true: +** +** (1) The isGDiff flags is true +** (2) The "gdiff-command" setting is undefined +** (3) There is a "tclsh" on PATH +** (4) There is a "wish" on PATH +*/ +int gdiff_using_tk(int isGdiff){ + if( isGdiff + && db_get("gdiff-command","")[0]==0 + && fossil_app_on_path("tclsh",0) + && fossil_app_on_path("wish",0) + ){ + return 1; + } + return 0; +} + +/* +** A TCL list is being constructed in pBlob. Append a single list +** element z[] to that list. Do all necessary quoting and escaping. +** The content of z[] might be controlled by an attacker. +*/ +static void diff_tcl_lappend(Blob *pBlob, const char *z, int isFilename){ + if( strchr(z,'{')==0 && strchr(z,'}')==0 ){ + if( isFilename ){ + blob_appendf(pBlob, " {%/}", z); + }else{ + blob_appendf(pBlob, " {%s}", z); + } + }else{ + int j; + blob_append_char(pBlob, ' '); + for(j=0; z[j]; j++){ + if( fossil_isalnum(z[j]) ){ + blob_append_char(pBlob, z[j]); + }else{ + blob_appendf(pBlob, "\\%03o", (unsigned char)z[j]); + } + } + } +} + +/* +** Return TRUE (non-zero) if zArg is a valid option to "diff" or +** "gdiff" that takes an argument that might be confused for a +** HOST:PATH or USER@HOST:PATH filename. +*/ +static int isDiffOptionWithArg(const char *zArg){ + int i; + const char *az[] = { + "binary", + "branch", + "ci", "checkin", + "r", "from", + "to" + }; + if( zArg[0]!='-' ) return 0; + zArg++; + if( zArg[0]=='-' ) zArg++; + for(i=0; i<count(az); i++){ + if( fossil_strcmp(zArg, az[i])==0 ) return 1; + } + return 0; +} + +/* +** Show diff output in a Tcl/Tk window, in response to the --tk option +** to the diff command. +** +** If fossil has direct access to a Tcl interpreter (either loaded +** dynamically through stubs or linked in statically), we can use it +** directly. Otherwise: +** (1) Write the Tcl/Tk script used for rendering into a temp file. +** (2) Invoke "tclsh" on the temp file using fossil_system(). +** (3) Delete the temp file. +*/ +void diff_tk(const char *zSubCmd, int firstArg){ + int i; + Blob script; + const char *zTempFile = 0; + char *zCmd; + const char *zTclsh; + const char *zHost = 0; + const char *zDir = 0; + int bDebug = find_option("tkdebug",0,0)!=0; + int bDarkMode = find_option("dark",0,0)!=0; + (void)find_option("debug",0,0); + blob_zero(&script); + /* Caution: When this routine is called from the merge-info command, + ** the --tcl argument requires an argument. But merge-info does not + ** use -i, so we can take -i as that argument. This routine needs to + ** always have -i after --tcl. + ** CAUTION! + ** vvvvvvv */ + blob_appendf(&script, "set fossilcmd {| \"%/\" %s -tcl -i -v", + g.nameOfExe, zSubCmd); + find_option("tcl",0,0); + find_option("html",0,0); + find_option("side-by-side","y",0); + find_option("internal","i",0); + find_option("verbose","v",0); + zTclsh = find_option("tclsh",0,1); + if( zTclsh==0 ){ + zTclsh = db_get("tclsh",0); + } + /* The undocumented --script FILENAME option causes the Tk script to + ** be written into the FILENAME instead of being run. This is used + ** for testing and debugging. */ + zTempFile = find_option("script",0,1); + for(i=firstArg; i<g.argc; i++){ + const char *z = g.argv[i]; + if( zHost==0 + && (i==firstArg || !isDiffOptionWithArg(g.argv[i-1])) + && !file_isfile_or_link(z) + && (zDir = file_skip_userhost(z))!=0 + ){ + zHost = mprintf("%.*s", (int)(zDir - z - 1), z); + continue; + } + diff_tcl_lappend(&script, z, 1); + } + blob_appendf(&script, "}\nset darkmode %d\n", bDarkMode); + blob_appendf(&script, "set debug %d\n", bDebug); + if( zHost ){ + blob_appendf(&script, "set remotehost"); + diff_tcl_lappend(&script, zHost, 0); + blob_appendf(&script, "\nset remotedir"); + diff_tcl_lappend(&script, zDir, 1); + blob_append_char(&script, '\n'); + } + blob_appendf(&script, "%s", builtin_file("diff.tcl", 0)); + if( zTempFile ){ + blob_write_to_file(&script, zTempFile); + fossil_print("To see diff, run: %s \"%s\"\n", zTclsh, zTempFile); + }else{ +#if defined(FOSSIL_ENABLE_TCL) + Th_FossilInit(TH_INIT_DEFAULT); + if( evaluateTclWithEvents(g.interp, &g.tcl, blob_str(&script), + blob_size(&script), 1, 1, 0)==TCL_OK ){ + blob_reset(&script); + return; + } + /* + * If evaluation of the Tcl script fails, the reason may be that Tk + * could not be found by the loaded Tcl, or that Tcl cannot be loaded + * dynamically (e.g. x64 Tcl with x86 Fossil). Therefore, fallback + * to using the external "tclsh", if available. + */ +#endif + zTempFile = write_blob_to_temp_file(&script); + zCmd = mprintf("%$ %$", zTclsh, zTempFile); + fossil_system(zCmd); + file_delete(zTempFile); + fossil_free(zCmd); + } + blob_reset(&script); +} + +/* +** Returns the GLOB pattern for file names that should be treated as binary +** by the diff subsystem, if any. +*/ +const char *diff_get_binary_glob(void){ + const char *zBinGlob = find_option("binary", 0, 1); + if( zBinGlob==0 ) zBinGlob = db_get("binary-glob",0); + return zBinGlob; +} + +/* +** COMMAND: diff +** COMMAND: gdiff +** +** Usage: %fossil diff|gdiff ?OPTIONS? ?FILE1? ?FILE2 ...? +** +** Show the difference between the current version of each of the FILEs +** specified (as they exist on disk) and that same file as it was checked- +** out. Or if the FILE arguments are omitted, show all committed changes +** currently in the working check-out. The "gdiff" variant means to +** use a GUI diff specified by the "gdiff-command" setting. If +** gdiff-command is unset or is an empty string, then "gdiff" behaves +** like "diff --tk". +** +** The default output format is a "unified patch" (the same as the +** output of "diff -u" on most unix systems). Many alternative formats +** are available. A few of the more useful alternatives: +** +** --tk Pop up a Tcl/Tk-based GUI to show the diff +** --by Show a side-by-side diff in the default web browser +** -b Show a linear diff in the default web browser +** -y Show a text side-by-side diff +** --webpage Format output as HTML +** --webpage -y HTML output in the side-by-side format +** +** The "--from VERSION" option is used to specify the source check-in +** for the diff operation. If not specified, the source check-in is the +** base check-in for the current check-out. Similarly, the "--to VERSION" +** option specifies the check-in from which the second version of the file +** or files is taken. If there is no "--to" option then the (possibly edited) +** files in the current check-out are used. The "--checkin VERSION" option +** shows the changes made by check-in VERSION relative to its primary parent. +** The "--branch BRANCHNAME" shows all the changes on the branch BRANCHNAME. +** +** With the "--from VERSION" option, if VERSION is actually a directory name +** (not a tag or check-in hash) then the files under that directory are used +** as the baseline for the diff. +** +** The "-i" command-line option forces the use of Fossil's own internal +** diff logic rather than any external diff program that might be configured +** using the "setting" command. If no external diff program is configured, +** then the "-i" option is a no-op. The "-i" option converts "gdiff" into +** "diff". +** +** The "--diff-binary" option enables or disables the inclusion of binary files +** when using an external diff program. +** +** The "--binary" option causes files matching the glob PATTERN to be treated +** as binary when considering if they should be used with the external diff +** program. This option overrides the "binary-glob" setting. +** +** If the "--tk" option is used (or "gdiff" is used and no third-party +** graphical diff tool is specified) then an additional argument of +** the form "USER@HOST:DIRECTORY" or "HOST:DIRECTORY" can be added to specify +** a remote host and directory from which the diff should be taken. If this +** happens, Fossil will run the diff using SSH, capture the results, then +** display the results locally using the Tcl/Tk diff GUI. +** +** These commands show differences between managed files. Use the "fossil xdiff" +** command to see differences in unmanaged files. +** +** Options: +** --binary PATTERN Treat files that match the glob PATTERN +** as binary +** --branch BRANCH Show diff of all changes on BRANCH +** --brief Show filenames only +** -b|--browser Show the diff output in a web-browser +** --by Shorthand for "--browser -y" +** -ci|--checkin VERSION Show diff of all changes in VERSION +** --command PROG External diff program. Overrides "diff-command" +** -c|--context N Show N lines of context around each change, +** with negative N meaning show all content +** --dark Use dark mode for the Tcl/Tk-based GUI and HTML +** --diff-binary BOOL Include binary files with external commands +** --exec-abs-paths Force absolute path names on external commands +** --exec-rel-paths Force relative path names on external commands +** -r|--from VERSION Use VERSION as the baseline for the diff, or +** if VERSION is a directory name, use files in +** that directory as the baseline. +** -w|--ignore-all-space Ignore white space when comparing lines +** -i|--internal Use internal diff logic +** --invert Invert the diff +** --json Output formatted as JSON +** -n|--linenum Show line numbers +** -N|--new-file Alias for --verbose +** -s|--numstat Show the number of added and deleted lines per +** file, omitting the diff. When combined +** with --brief, show only the total row. +** -y|--side-by-side Side-by-side diff +** --strip-trailing-cr Strip trailing CR +** --tcl Tcl-formatted output used internally by --tk +** --tclsh PATH Tcl/Tk shell used for --tk (default: "tclsh") +** --tk Launch a Tcl/Tk GUI for display +** --to VERSION Select VERSION as target for the diff +** --undo Use the undo buffer as the baseline +** --unified Unified diff +** -v|--verbose Output complete text of added or deleted files +** -h|--versions Show compared versions in the diff header +** --webpage Format output as a stand-alone HTML webpage +** -W|--width N Width of lines in side-by-side diff +** -Z|--ignore-trailing-space Ignore changes to end-of-line whitespace +*/ +void diff_cmd(void){ + int isGDiff; /* True for gdiff. False for normal diff */ + const char *zFrom; /* Source version number */ + const char *zTo; /* Target version number */ + const char *zCheckin; /* Check-in version number */ + const char *zBranch; /* Branch to diff */ + int againstUndo = 0; /* Diff against files in the undo buffer */ + FileDirList *pFileDir = 0; /* Restrict the diff to these files */ + DiffConfig DCfg; /* Diff configuration object */ + int bFromIsDir = 0; /* True if zFrom is a directory name */ + + isGDiff = g.argv[1][0]=='g'; + if( find_option("tk",0,0)!=0 + || has_option("tclsh") + || gdiff_using_tk(isGDiff) + ){ + diff_tk("diff", 2); + return; + } + zFrom = find_option("from", "r", 1); + zTo = find_option("to", 0, 1); + zCheckin = find_option("checkin", "ci", 1); + zBranch = find_option("branch", 0, 1); + againstUndo = find_option("undo",0,0)!=0; + if( againstUndo && (zFrom!=0 || zTo!=0 || zCheckin!=0 || zBranch!=0) ){ + fossil_fatal("cannot use --undo together with --from, --to, --checkin," + " or --branch"); + } + if( zBranch ){ + if( zTo || zFrom || zCheckin ){ + fossil_fatal("cannot use --from, --to, or --checkin with --branch"); + } + zTo = zBranch; + zFrom = mprintf("merge-in:%s", zBranch); + zBranch = 0; + } + if( zCheckin!=0 && (zFrom!=0 || zTo!=0) ){ + fossil_fatal("cannot use --checkin together with --from or --to"); + } + if( 0==zCheckin ){ + if( zTo==0 || againstUndo ){ + db_must_be_within_tree(); + }else if( zFrom==0 ){ + fossil_fatal("must use --from if --to is present"); + }else{ + db_find_and_open_repository(0, 0); + } + }else{ + db_find_and_open_repository(0, 0); + } + determine_exec_relative_option(1); + if( zFrom!=file_tail(zFrom) + && file_isdir(zFrom, ExtFILE)==1 + && !db_exists("SELECT 1 FROM tag WHERE tagname='sym-%q'", zFrom) + ){ + bFromIsDir = 1; + if( zTo ){ + fossil_fatal("cannot use --to together with \"--from PATH\""); + } + } + diff_options(&DCfg, isGDiff, 0); + verify_all_options(); + g.diffCnt[0] = g.diffCnt[1] = g.diffCnt[2] = 0; + if( g.argc>=3 ){ + int i; + Blob fname; + pFileDir = fossil_malloc( sizeof(*pFileDir) * (g.argc-1) ); + memset(pFileDir, 0, sizeof(*pFileDir) * (g.argc-1)); + for(i=2; i<g.argc; i++){ + file_tree_name(g.argv[i], &fname, 0, 1); + pFileDir[i-2].zName = fossil_strdup(blob_str(&fname)); + if( strcmp(pFileDir[i-2].zName,".")==0 ){ + pFileDir[0].zName[0] = '.'; + pFileDir[0].zName[1] = 0; + break; + } + pFileDir[i-2].nName = blob_size(&fname); + pFileDir[i-2].nUsed = 0; + blob_reset(&fname); + } + } + if( (DCfg.diffFlags & DIFF_NUMSTAT) && !(DCfg.diffFlags & DIFF_BRIEF) ){ + fossil_print("%10s %10s\n", "INSERTED", "DELETED"); + } + if( zCheckin!=0 ){ + int ridTo = name_to_typed_rid(zCheckin, "ci"); + zTo = zCheckin; + zFrom = db_text(0, + "SELECT uuid FROM blob, plink" + " WHERE plink.cid=%d AND plink.isprim AND plink.pid=blob.rid", + ridTo); + if( zFrom==0 ){ + fossil_fatal("check-in %s has no parent", zTo); + } + } + diff_begin(&DCfg); + if( bFromIsDir ){ + diff_externbase_to_checkout(zFrom, &DCfg, pFileDir); + }else if( againstUndo ){ + if( db_lget_int("undo_available",0)==0 ){ + fossil_print("No undo or redo is available\n"); + return; + } + diff_undo_to_checkout(&DCfg, pFileDir); + }else if( zTo==0 ){ + diff_version_to_checkout(zFrom, &DCfg, pFileDir, 0); + }else{ + diff_two_versions(zFrom, zTo, &DCfg, pFileDir); + } + if( pFileDir ){ + int i; + for(i=0; pFileDir[i].zName; i++){ + if( pFileDir[i].nUsed==0 + && strcmp(pFileDir[0].zName,".")!=0 + && !file_isdir(g.argv[i+2], ExtFILE) + ){ + fossil_fatal("not found: '%s'", g.argv[i+2]); + } + fossil_free(pFileDir[i].zName); + } + fossil_free(pFileDir); + } + diff_end(&DCfg, 0); + if ( DCfg.diffFlags & DIFF_NUMSTAT ){ + fossil_print("%10d %10d TOTAL over %d changed file%s\n", + g.diffCnt[1], g.diffCnt[2], g.diffCnt[0], g.diffCnt[0]!=1 ? "s": ""); + } +} + +/* +** WEBPAGE: vpatch +** URL: /vpatch?from=FROM&to=TO +** +** Show a patch that goes from check-in FROM to check-in TO. +*/ +void vpatch_page(void){ + const char *zFrom = P("from"); + const char *zTo = P("to"); + DiffConfig DCfg; + cgi_check_for_malice(); + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + if( zFrom==0 || zTo==0 ) fossil_redirect_home(); + if( robot_restrict("diff") ) return; + + fossil_nice_default(); + cgi_set_content_type("text/plain"); + diff_config_init(&DCfg, DIFF_VERBOSE); + diff_two_versions(zFrom, zTo, &DCfg, 0); +} diff --git a/src/dispatch.c b/src/dispatch.c new file mode 100644 index 0000000..9a464a4 --- /dev/null +++ b/src/dispatch.c @@ -0,0 +1,1971 @@ +/* +** Copyright (c) 2016 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file contains code used to map command names (ex: "help", "commit", +** "diff") or webpage names (ex: "/timeline", "/search") into the functions +** that implement those commands and web pages and their associated help +** text. +*/ +#include "config.h" +#include <assert.h> +#include "dispatch.h" + +#if INTERFACE +/* +** An instance of this object defines everything we need to know about an +** individual command, webpage, setting, or help topic. +*/ +struct CmdOrPage { + const char *zName; /* Name. Webpages start with "/". Commands do not */ + void (*xFunc)(void); /* Implementation function, or NULL for settings */ + const char *zHelp; /* Raw help text */ + int iHelp; /* Index of help variable */ + unsigned int eCmdFlags; /* Flags */ +}; + +/*************************************************************************** +** These macros must match similar macros in mkindex.c +** Allowed values for CmdOrPage.eCmdFlags. +*/ +#define CMDFLAG_1ST_TIER 0x000001 /* Most important commands */ +#define CMDFLAG_2ND_TIER 0x000002 /* Obscure and seldom used commands */ +#define CMDFLAG_TEST 0x000004 /* Commands for testing only */ +#define CMDFLAG_WEBPAGE 0x000008 /* Web pages */ +#define CMDFLAG_COMMAND 0x000010 /* A command */ +#define CMDFLAG_SETTING 0x000020 /* A setting */ +#define CMDFLAG_VERSIONABLE 0x000040 /* A versionable setting */ +#define CMDFLAG_BLOCKTEXT 0x000080 /* Multi-line text setting */ +#define CMDFLAG_BOOLEAN 0x000100 /* A boolean setting */ +#define CMDFLAG_RAWCONTENT 0x000200 /* Do not interpret POST content */ +/* NOTE: 0x000400 = CMDFLAG_SENSITIVE in mkindex.c! */ +#define CMDFLAG_HIDDEN 0x000800 /* Elide from most listings */ +#define CMDFLAG_LDAVG_EXEMPT 0x001000 /* Exempt from load_control() */ +#define CMDFLAG_ALIAS 0x002000 /* Command aliases */ +#define CMDFLAG_KEEPEMPTY 0x004000 /* Do not unset empty settings */ +#define CMDFLAG_ABBREVSUBCMD 0x008000 /* Help text abbreviates subcommands */ +#define CMDFLAG_TOPIC 0x010000 /* A help topic */ +/**************************************************************************/ + +/* Values for the 2nd parameter to dispatch_name_search() */ +#define CMDFLAG_ANY 0x010038 /* Match anything */ +#define CMDFLAG_PREFIX 0x000200 /* Prefix match is OK */ + +#endif /* INTERFACE */ + +/* +** The page_index.h file contains the definition for aCommand[] - an array +** of CmdOrPage objects that defines all available commands and webpages +** known to Fossil. +** +** The entries in aCommand[] are in sorted order by name. Since webpage names +** always begin with "/", all webpage names occur first. The page_index.h file +** also sets the FOSSIL_FIRST_CMD macro to be the *approximate* index +** in aCommand[] of the first command entry. FOSSIL_FIRST_CMD might be +** slightly too low, and so the range FOSSIL_FIRST_CMD...MX_COMMAND might +** contain a few webpage entries at the beginning. +** +** The page_index.h file is generated by the mkindex program which scans all +** source code files looking for header comments on the functions that +** implement command and webpages. +*/ +#include "page_index.h" +#define MX_COMMAND count(aCommand) +#define MX_HELP_DUP 5 /* Upper bound estimate on help string duplication */ + +/* +** Given a command, webpage, or setting name in zName, find the corresponding +** CmdOrPage object and return a pointer to that object in *ppCmd. +** +** The eType field is CMDFLAG_COMMAND to look up commands, CMDFLAG_WEBPAGE to +** look up webpages, CMDFLAG_SETTING to look up settings, or CMDFLAG_ANY to look +** for any. If the CMDFLAG_PREFIX bit is set, then a prefix match is allowed. +** +** Return values: +** 0: Success. *ppCmd is set to the appropriate CmdOrPage +** 1: Not found. +** 2: Ambiguous. Two or more entries match. +*/ +int dispatch_name_search( + const char *zName, /* Look for this name */ + unsigned eType, /* CMDFLAGS_* bits */ + const CmdOrPage **ppCmd /* Write the matching CmdOrPage object here */ +){ + int upr, lwr, mid; + int nName = strlen(zName); + lwr = 0; + upr = MX_COMMAND - 1; + while( lwr<=upr ){ + int c; + mid = (upr+lwr)/2; + c = strcmp(zName, aCommand[mid].zName); + if( c==0 ){ + if( (aCommand[mid].eCmdFlags & eType)==0 ) return 1; + *ppCmd = &aCommand[mid]; + return 0; /* An exact match */ + }else if( c<0 ){ + upr = mid - 1; + }else{ + lwr = mid + 1; + } + } + if( (eType & CMDFLAG_PREFIX)!=0 + && lwr<MX_COMMAND + && strncmp(zName, aCommand[lwr].zName, nName)==0 + ){ + /* An inexact prefix match was found. Scan the name table to try to find + * exactly one entry with this prefix and the requested type. */ + for( mid=-1; lwr<MX_COMMAND + && strncmp(zName, aCommand[lwr].zName, nName)==0; ++lwr ){ + if( aCommand[lwr].eCmdFlags & eType ){ + if( mid<0 ){ + mid = lwr; /* Potential ambiguous prefix */ + }else{ + if( aCommand[lwr].xFunc != aCommand[mid].xFunc ){ + return 2; /* Confirmed ambiguous prefix */ + } + } + } + } + if( mid>=0 ){ + *ppCmd = &aCommand[mid]; + return 0; /* Prefix match */ + } + } + return 1; /* Not found */ +} + +/* +** zName is the name of a webpage (eType==CMDFLAGS_WEBPAGE) that does not +** exist in the dispatch table. Check to see if this webpage name exists +** as an alias in the CONFIG table of the repository. If it is, then make +** appropriate changes to the CGI environment and set *ppCmd to point to the +** aliased command. +** +** Return 0 if the command is successfully aliased. Return 1 if there +** is not alias for zName. Any kind of error in the alias value causes a +** error to be thrown. +** +** Alias entries in the CONFIG table have a "name" value of "walias:NAME" +** where NAME is the input page name. The value is a string of the form +** "NEWNAME?QUERYPARAMS". The ?QUERYPARAMS is optional. If present (and it +** usually is), then all query parameters are added to the CGI environment. +** Except, query parameters of the form "X!" cause any CGI X variable to be +** removed. +*/ +int dispatch_alias(const char *zName, const CmdOrPage **ppCmd){ + char *z; + char *zQ; + int i; + + z = db_text(0, "SELECT value FROM config WHERE name='walias:%q'",zName); + if( z==0 ) return 1; + for(i=0; z[i] && z[i]!='?'; i++){} + if( z[i]=='?' ){ + z[i] = 0; + zQ = &z[i+1]; + }else{ + zQ = &z[i]; + } + if( dispatch_name_search(z, CMDFLAG_WEBPAGE, ppCmd) ){ + fossil_fatal("\"%s\" aliased to \"%s\" but \"%s\" does not exist", + zName, z, z); + } + z = zQ; + while( *z ){ + char *zName = z; + char *zValue = 0; + while( *z && *z!='=' && *z!='&' && *z!='!' ){ z++; } + if( *z=='=' ){ + *z = 0; + z++; + zValue = z; + while( *z && *z!='&' ){ z++; } + if( *z ){ + *z = 0; + z++; + } + dehttpize(zValue); + }else if( *z=='!' ){ + *(z++) = 0; + cgi_delete_query_parameter(zName); + zName = ""; + }else{ + if( *z ){ *z++ = 0; } + zValue = ""; + } + if( fossil_islower(zName[0]) ){ + cgi_replace_query_parameter(zName, zValue); + }else if( fossil_isupper(zName[0]) ){ + cgi_replace_query_parameter_tolower(zName, zValue); + } + } + return 0; +} + +/* +** Fill Blob with a space-separated list of all command names that +** match the prefix zPrefix and the eType CMDFLAGS_ bits. +*/ +void dispatch_matching_names( + const char *zPrefix, /* name prefix */ + unsigned eType, /* CMDFLAG_ bits */ + Blob *pList /* space-separated list of command names */ +){ + int i; + int nPrefix = (int)strlen(zPrefix); + for(i=FOSSIL_FIRST_CMD; i<MX_COMMAND; i++){ + if( (aCommand[i].eCmdFlags & eType)==0 ) continue; + if( strncmp(zPrefix, aCommand[i].zName, nPrefix)==0 ){ + blob_appendf(pList, " %s", aCommand[i].zName); + } + } +} + +/* +** Return the index of the first non-space character that follows +** a span of two or more spaces. Return 0 if there is not gap. +*/ +static int hasGap(const char *z, int n){ + int i; + for(i=3; i<n-1; i++){ + if( z[i]==' ' && z[i+1]!=' ' && z[i-1]==' ' && z[i-2]!='.' ) return i+1; + } + return 0 ; +} + +/* +** Input string zIn starts with '['. If the content is a hyperlink of the +** form [[...]] then return the index of the closing ']'. Otherwise return 0. +*/ +static int help_is_link(const char *z, int n){ + int i; + char c; + if( n<5 ) return 0; + if( z[1]!='[' ) return 0; + for(i=3; i<n && (c = z[i])!=0; i++){ + if( c==']' && z[i-1]==']' ) return i; + } + return 0; +} + +/* +** Append text to pOut with changes: +** +** * Add hyperlink markup for [[...]] +** * Escape HTML characters: < > & and " +** * Change "%fossil" to just "fossil" +*/ +static void appendLinked(Blob *pOut, const char *z, int n){ + int i = 0; + int j; + while( i<n ){ + char c = z[i]; + if( c=='[' && (j = help_is_link(z+i, n-i))>0 ){ + if( i ) blob_append(pOut, z, i); + z += i+2; + n -= i+2; + blob_appendf(pOut, "<a href='%R/help/%.*s'>%.*s</a>", + j-3, z, j-3, z); + z += j-1; + n -= j-1; + i = 0; + }else if( c=='%' && n-i>=7 && strncmp(z+i,"%fossil",7)==0 ){ + if( i ) blob_append(pOut, z, i); + z += i+7; + n -= i+7; + blob_append(pOut, "fossil", 6); + i = 0; + }else if( c=='<' ){ + if( i ) blob_append(pOut, z, i); + blob_append(pOut, "<", 4); + z += i+1; + n -= i+1; + i = 0; + }else if( c=='>' ){ + if( i ) blob_append(pOut, z, i); + blob_append(pOut, ">", 4); + z += i+1; + n -= i+1; + i = 0; + }else if( c=='&' ){ + if( i ) blob_append(pOut, z, i); + blob_append(pOut, "&", 5); + z += i+1; + n -= i+1; + i = 0; + }else{ + i++; + } + } + blob_append(pOut, z, i); +} + +/* +** Append text to pOut, adding formatting markup. Terms that +** have all lower-case letters are within <tt>..</tt>. Terms +** that have all upper-case letters are within <i>..</i>. +*/ +static void appendMixedFont(Blob *pOut, const char *z, int n){ + const char *zEnd = ""; + int i = 0; + int j; + while( i<n ){ + if( z[i]==' ' || z[i]=='=' ){ + for(j=i+1; j<n && (z[j]==' ' || z[j]=='='); j++){} + appendLinked(pOut, z+i, j-i); + i = j; + }else{ + for(j=i; j<n && z[j]!=' ' && z[j]!='=' && !fossil_isalpha(z[j]); j++){} + if( j>=n || z[j]==' ' || z[j]=='=' ){ + zEnd = ""; + }else{ + if( fossil_isupper(z[j]) && z[i]!='-' ){ + blob_append(pOut, "<i>",3); + zEnd = "</i>"; + }else{ + blob_append(pOut, "<tt>", 4); + zEnd = "</tt>"; + } + } + while( j<n && z[j]!=' ' && z[j]!='=' ){ j++; } + appendLinked(pOut, z+i, j-i); + if( zEnd[0] ) blob_append(pOut, zEnd, -1); + i = j; + } + } +} + +/* +** Attempt to reformat plain-text help into HTML for display on a webpage. +** +** The HTML output is appended to Blob pHtml, which should already be +** initialized. +** +** Formatting rules: +** +** * Bullet lists are indented from the surrounding text by +** at least one space. Each bullet begins with " * ". +** +** * Display lists are indented from the surrounding text. +** Each tag begins with "-" or occur on a line that is +** followed by two spaces and a non-space. <dd> elements can begin +** on the same line as long as they are separated by at least +** two spaces. +** +** * Indented text is show verbatim (<pre>...</pre>) +** +** * Lines that begin with "|" at the left margin are in <pre>...</pre> +*/ +static void help_to_html(const char *zHelp, Blob *pHtml){ + int i; + char c; + int nIndent = 0; + int wantP = 0; + int wantBR = 0; + int aIndent[10]; + const char *azEnd[10]; + int iLevel = 0; + int isLI = 0; + int isDT = 0; + int inPRE = 0; + static const char *zEndDL = "</dl></blockquote>"; + static const char *zEndPRE = "</pre></blockquote>"; + static const char *zEndUL = "</ul>"; + static const char *zEndDD = "</dd>"; + + aIndent[0] = 0; + azEnd[0] = ""; + while( zHelp[0] ){ + i = 0; + while( (c = zHelp[i])!=0 && c!='\n' ){ + if( c=='%' && i>2 && zHelp[i-2]==':' && strncmp(zHelp+i,"%fossil",7)==0 ){ + appendLinked(pHtml, zHelp, i); + zHelp += i+1; + i = 0; + wantBR = 1; + continue; + } + i++; + } + if( i>2 && (zHelp[0]=='>' || zHelp[0]=='|') && zHelp[1]==' ' ){ + if( zHelp[0]=='>' ){ + isDT = 1; + for(nIndent=1; nIndent<i && zHelp[nIndent]==' '; nIndent++){} + }else{ + if( !inPRE ){ + blob_append(pHtml, "<pre>\n", -1); + inPRE = 1; + } + } + }else{ + if( inPRE ){ + blob_append(pHtml, "</pre>\n", -1); + inPRE = 0; + } + isDT = 0; + for(nIndent=0; nIndent<i && zHelp[nIndent]==' '; nIndent++){} + } + if( inPRE ){ + blob_append(pHtml, zHelp+1, i); + zHelp += i + 1; + continue; + } + if( nIndent==i ){ + if( c==0 ) break; + if( iLevel && azEnd[iLevel]==zEndPRE ){ + /* Skip the newline at the end of a <pre> */ + }else{ + blob_append_char(pHtml, '\n'); + } + wantP = 1; + wantBR = 0; + zHelp += i+1; + continue; + } + if( nIndent+2<i && zHelp[nIndent]=='*' && zHelp[nIndent+1]==' ' ){ + nIndent += 2; + while( nIndent<i && zHelp[nIndent]==' '){ nIndent++; } + isLI = 1; + }else{ + isLI = 0; + } + while( iLevel>0 && aIndent[iLevel]>nIndent ){ + blob_append(pHtml, azEnd[iLevel--], -1); + } + if( nIndent>aIndent[iLevel] ){ + assert( iLevel<ArraySize(aIndent)-2 ); + if( isLI ){ + iLevel++; + aIndent[iLevel] = nIndent; + azEnd[iLevel] = zEndUL; + if( wantP ){ + blob_append(pHtml,"<p>", 3); + wantP = 0; + } + blob_append(pHtml, "<ul>\n", 5); + }else if( isDT + || zHelp[nIndent]=='-' + || hasGap(zHelp+nIndent,i-nIndent) ){ + iLevel++; + aIndent[iLevel] = nIndent; + azEnd[iLevel] = zEndDL; + wantP = 0; + if( isDT ){ + blob_append(pHtml, "<blockquote><dl>\n", -1); + }else{ + blob_append(pHtml, "<blockquote><dl class=\"helpOptions\">\n", -1); + } + }else if( azEnd[iLevel]==zEndDL ){ + iLevel++; + aIndent[iLevel] = nIndent; + azEnd[iLevel] = zEndDD; + if( wantP ){ + blob_append(pHtml,"<p>", 3); + wantP = 0; + } + blob_append(pHtml, "<dd>", 4); + }else if( wantP ){ + iLevel++; + aIndent[iLevel] = nIndent; + azEnd[iLevel] = zEndPRE; + blob_append(pHtml, "<blockquote><pre>", -1); + wantP = 0; + } + } + if( isLI ){ + blob_append(pHtml, "<li> ", 5); + } + if( wantP ){ + blob_append(pHtml, "<p> ", 4); + wantP = 0; + } + if( azEnd[iLevel]==zEndDL ){ + int iDD; + blob_append(pHtml, "<dt> ", 5); + iDD = hasGap(zHelp+nIndent, i-nIndent); + if( iDD ){ + int x; + assert( iLevel<ArraySize(aIndent)-1 ); + iLevel++; + aIndent[iLevel] = x = nIndent+iDD; + azEnd[iLevel] = zEndDD; + appendMixedFont(pHtml, zHelp+nIndent, iDD-2); + blob_append(pHtml, "</dt><dd>",9); + appendLinked(pHtml, zHelp+x, i-x); + }else{ + appendMixedFont(pHtml, zHelp+nIndent, i-nIndent); + } + blob_append(pHtml, "</dt>\n", 6); + }else if( wantBR ){ + appendMixedFont(pHtml, zHelp+nIndent, i-nIndent); + blob_append(pHtml, "<br>\n", 5); + wantBR = 0; + }else{ + appendLinked(pHtml, zHelp+nIndent, i-nIndent); + blob_append_char(pHtml, '\n'); + } + zHelp += i+1; + i = 0; + if( c==0 ) break; + } + while( iLevel>0 ){ + blob_appendf(pHtml, "%s\n", azEnd[iLevel--]); + } +} + +/* +** Format help text for TTY display. +*/ +static void help_to_text(const char *zHelp, Blob *pText, int bUsage){ + int i, x; + char c; + if( zHelp[0]=='>' ){ + if( !bUsage ){ + blob_appendf(pText, "Usage:"); + }else{ + blob_append_char(pText, ' '); + } + zHelp++; + } + for(i=0; (c = zHelp[i])!=0; i++){ + if( c=='%' && strncmp(zHelp+i,"%fossil",7)==0 ){ + if( i>0 ) blob_append(pText, zHelp, i); + blob_append(pText, "fossil", 6); + zHelp += i+7; + i = -1; + continue; + } + if( c=='\n' && (zHelp[i+1]=='>' || zHelp[i+1]=='|') && zHelp[i+2]==' ' ){ + blob_append(pText, zHelp, i+1); + blob_append(pText, " ", 1); + zHelp += i+2; + i = -1; + continue; + } + if( c=='[' && (x = help_is_link(zHelp+i, 100000))!=0 ){ + if( i>0 ) blob_append(pText, zHelp, i); + zHelp += i+2; + blob_append(pText, zHelp, x-3); + zHelp += x-1; + i = -1; + continue; + } + } + if( i>0 ){ + blob_append(pText, zHelp, i); + } +} + +/* +** Display help for all commands based on provided flags. +*/ +static void display_all_help(int mask, int useHtml, int rawOut){ + int i; + unsigned char occHelp[FOSSIL_MX_CMDIDX] = {0}; /* Help string occurrences */ + int bktHelp[FOSSIL_MX_CMDIDX][MX_HELP_DUP] = {{0}};/* Help strings->commands*/ + if( useHtml ) fossil_print("<!--\n"); + fossil_print("Help text for:\n"); + if( mask & CMDFLAG_1ST_TIER ) fossil_print(" * Commands\n"); + if( mask & CMDFLAG_2ND_TIER ) fossil_print(" * Auxiliary commands\n"); + if( mask & CMDFLAG_ALIAS ) fossil_print(" * Aliases\n"); + if( mask & CMDFLAG_TEST ) fossil_print(" * Test commands\n"); + if( mask & CMDFLAG_WEBPAGE ) fossil_print(" * Web pages\n"); + if( mask & CMDFLAG_SETTING ) fossil_print(" * Settings\n"); + if( mask & CMDFLAG_TOPIC ) fossil_print(" * Help Topic\n"); + if( useHtml ){ + fossil_print("-->\n"); + fossil_print("<!-- start_all_help -->\n"); + }else{ + fossil_print("---\n"); + } + /* Fill in help string buckets */ + for(i=0; i<MX_COMMAND; i++){ + if( (aCommand[i].eCmdFlags & mask)==0 ) continue; + else if(aCommand[i].eCmdFlags & CMDFLAG_HIDDEN) continue; + bktHelp[aCommand[i].iHelp][occHelp[aCommand[i].iHelp]++] = i; + } + for(i=0; i<MX_COMMAND; i++){ + if( (aCommand[i].eCmdFlags & mask)==0 ) continue; + else if(aCommand[i].eCmdFlags & CMDFLAG_HIDDEN) continue; + if( occHelp[aCommand[i].iHelp] > 0 ){ + int j; + if( useHtml ){ + Blob html; + blob_init(&html, 0, 0); + help_to_html(aCommand[i].zHelp, &html); + for(j=0; j<occHelp[aCommand[i].iHelp]; j++){ + fossil_print("<h1>%h</h1>\n", + aCommand[bktHelp[aCommand[i].iHelp][j]].zName); + } + fossil_print("%s\n<hr>\n", blob_str(&html)); + blob_reset(&html); + }else if( rawOut ){ + for(j=0; j<occHelp[aCommand[i].iHelp]; j++) + fossil_print("# %s\n", aCommand[bktHelp[aCommand[i].iHelp][j]].zName); + fossil_print("%s\n\n", aCommand[i].zHelp); + }else{ + Blob txt; + blob_init(&txt, 0, 0); + help_to_text(aCommand[i].zHelp, &txt, 0); + for(j=0; j<occHelp[aCommand[i].iHelp]; j++){ + fossil_print("# %s%s\n", + aCommand[bktHelp[aCommand[i].iHelp][j]].zName, + (aCommand[i].eCmdFlags & CMDFLAG_VERSIONABLE)!=0 ? + " (versionable)" : ""); + } + fossil_print("%s\n\n", blob_str(&txt)); + blob_reset(&txt); + } + occHelp[aCommand[i].iHelp] = 0; + } + } + if( useHtml ){ + fossil_print("<!-- end_all_help -->\n"); + }else{ + fossil_print("---\n"); + } + version_cmd(); +} + +/* +** COMMAND: test-all-help +** +** Usage: %fossil test-all-help ?OPTIONS? +** +** Show help text for commands and pages. Useful for proof-reading. +** Defaults to just the CLI commands. Specify --www to see only the +** web pages, or --everything to see both commands and pages. +** +** Options: +** -a|--aliases Show aliases +** -c|--topics Show help topics +** -e|--everything Show all commands and pages. Omit aliases to +** avoid duplicates. +** -h|--html Transform output to HTML +** -o|--options Show global options +** -r|--raw No output formatting +** -s|--settings Show settings +** -t|--test Include test- commands +** -w|--www Show WWW pages +*/ +void test_all_help_cmd(void){ + int mask = CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER; + int useHtml = find_option("html","h",0)!=0; + int rawOut = find_option("raw","r",0)!=0; + + if( find_option("www","w",0) ){ + mask = CMDFLAG_WEBPAGE; + } + if( find_option("everything","e",0) ){ + mask = CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER | CMDFLAG_WEBPAGE | + CMDFLAG_ALIAS | CMDFLAG_SETTING | CMDFLAG_TEST | CMDFLAG_TOPIC; + } + if( find_option("settings","s",0) ){ + mask = CMDFLAG_SETTING; + } + if( find_option("aliases","a",0) ){ + mask = CMDFLAG_ALIAS; + } + if( find_option("test","t",0) ){ + mask |= CMDFLAG_TEST; + } + if( find_option("topics","c",0) ){ + mask |= CMDFLAG_TOPIC; + } + display_all_help(mask, useHtml, rawOut); +} + +/* +** Count the number of entries in the aCommand[] table that match +** the given flag. +*/ +static int countCmds(unsigned int eFlg){ + int n = 0; + int i; + for(i=0; i<MX_COMMAND; i++){ + if( (aCommand[i].eCmdFlags & eFlg)!=0 ) n++; + } + return n; +} + +/* +** COMMAND: test-command-stats +** +** Print statistics about the built-in command dispatch table. +*/ +void test_command_stats_cmd(void){ + fossil_print("commands: %4d\n", + countCmds( CMDFLAG_COMMAND )); + fossil_print(" 1st tier %4d\n", + countCmds( CMDFLAG_1ST_TIER )); + fossil_print(" 2nd tier %4d\n", + countCmds( CMDFLAG_2ND_TIER )); + fossil_print(" alias %4d\n", + countCmds( CMDFLAG_ALIAS )); + fossil_print(" test %4d\n", + countCmds( CMDFLAG_TEST )); + fossil_print("web-pages: %4d\n", + countCmds( CMDFLAG_WEBPAGE )); + fossil_print("settings: %4d\n", + countCmds( CMDFLAG_SETTING )); + fossil_print("help-topics: %4d\n", + countCmds( CMDFLAG_TOPIC )); + fossil_print("total entries: %4d\n", MX_COMMAND); +} + +/* +** Compute an estimate of the edit-distance between to input strings. +** +** The first string is the input. The second is the pattern. Only the +** first 100 characters of the pattern are considered. +*/ +static int edit_distance(const char *zA, const char *zB){ + int nA = (int)strlen(zA); + int nB = (int)strlen(zB); + int i, j, m; + int p0, p1, c0; + int a[100] = {0}; + static const int incr = 4; + + for(j=0; j<nB; j++) a[j] = 1; + for(i=0; i<nA; i++){ + p0 = i==0 ? 0 : i*incr-1; + c0 = i*incr; + for(j=0; j<nB; j++){ + int m = 999; + p1 = a[j]; + if( zA[i]==zB[j] ){ + m = p0; + }else{ + m = c0+2; + if( m>p1+2 ) m = p1+2; + if( m>p0+3 ) m = p0+3; + } + c0 = a[j]; + a[j] = m; + p0 = p1; + } + } + m = a[nB-1]; + for(j=0; j<nB-1; j++){ + if( a[j]+1<m ) m = a[j]+1; + } + return m; +} + +/* +** Fill the pointer array with names of commands that approximately +** match the input. Return the number of approximate matches. +** +** Closest matches appear first. +*/ +int dispatch_approx_match(const char *zIn, int nArray, const char **azArray){ + int i; + int bestScore; + int m; + int n = 0; + int mnScore = 0; + int mxScore = 99999; + int iFirst, iLast; + + if( zIn[0]=='/' ){ + iFirst = 0; + iLast = FOSSIL_FIRST_CMD-1; + }else{ + iFirst = FOSSIL_FIRST_CMD; + iLast = MX_COMMAND-1; + } + + while( n<nArray ){ + bestScore = mxScore; + for(i=iFirst; i<=iLast; i++){ + m = edit_distance(zIn, aCommand[i].zName); + if( m<mnScore ) continue; + if( m==mnScore ){ + azArray[n++] = aCommand[i].zName; + if( n>=nArray ) return n; + }else if( m<bestScore ){ + bestScore = m; + } + } + if( bestScore>=mxScore ) break; + mnScore = bestScore; + } + return n; +} + +/* +** COMMAND: test-approx-match +** +** Test the approximate match algorithm +*/ +void test_approx_match_command(void){ + int i, j, n; + const char *az[20]; + for(i=2; i<g.argc; i++){ + fossil_print("%s:\n", g.argv[i]); + n = dispatch_approx_match(g.argv[i], 20, az); + for(j=0; j<n; j++){ + fossil_print(" %s\n", az[j]); + } + } +} + + +/* +** Returns 1 if the command or page name zName is known to be a +** command/page which is only available in certain builds/platforms, +** else returns 0. +*/ +static int help_is_platform_command(const char *zName){ + const char *aList[] = { + /* List of commands/pages which are known to only be available in + ** certain builds/platforms. */ + "winsrv", + "json", "/json", + NULL /* end-of-list sentinel */ + }; + int i = 0; + const char *z; + for( z = aList[0]; z ; z = aList[++i] ){ + if( 0==fossil_strcmp(zName, z) ) return 1; + } + return 0; +} + +/* +** WEBPAGE: help +** URL: /help/CMD or /help/www/PAGE +** +** Show the built-in help text for CMD or PAGE. CMD can be a command-line +** interface command or a setting name. PAGE is the name of a +** web interface. /help//PAGE also works if the double-/ makes it through +** the main web server. +** +** Query parameters: +** +** name=CMD Show help for CMD where CMD is a command name or +** or setting name. If CMD beings with "/" it is +** interpreted as a PAGE name. +** +** name=www/PAGE Show help for web page PAGE. +** +** name=/PAGE The initial "www/" on web-page help can be abbreviated as +** just "/" +** +** plaintext Show the help within <pre>...</pre>, as if it were +** displayed using the "fossil help" command. +** +** raw Show the raw help text without any formatting. +** (Used for debugging.) +*/ +void help_page(void){ + const char *zCmd = P("cmd"); + + if( zCmd==0 ) zCmd = P("name"); + cgi_check_for_malice(); + if( zCmd && *zCmd ){ + int rc; + const CmdOrPage *pCmd = 0; + + style_set_current_feature("tkt"); + style_submenu_element("Topic-List", "%R/help"); + if( search_restrict(SRCH_HELP)!=0 ){ + style_submenu_element("Search","%R/search?y=h"); + } + if( strncmp(zCmd,"www/",4)==0 && zCmd[4]!=0 ){ + /* Use https://domain/fossil/help/www/timeline or similar with the "www" + ** intermediate tag to view web-page documentation. */ + zCmd += 3; + } + rc = dispatch_name_search(zCmd, CMDFLAG_ANY|CMDFLAG_PREFIX, &pCmd); + if( pCmd ){ + style_header("Help: %s", pCmd->zName); + }else{ + style_header("Help"); + } + if( pCmd==0 ){ + /* No <h1> line in this case */ + }else if( *zCmd=='/' ){ + /* Some of the webpages require query parameters in order to work. + ** @ <h1>The "<a href='%R%s(zCmd)'>%s(zCmd)</a>" page:</h1> */ + @ <h1>The "%h(pCmd->zName)" page:</h1> + }else if( rc==0 && (pCmd->eCmdFlags & CMDFLAG_SETTING)!=0 ){ + @ <h1>The "%h(pCmd->zName)" setting:</h1> + }else if( rc==0 && (pCmd->eCmdFlags & CMDFLAG_TOPIC)!=0 ){ + @ <h1>The "%h(pCmd->zName)" help topic:</h1> + }else{ + @ <h1>The "%h(pCmd->zName)" command:</h1> + } + if( rc==1 || (rc==2 && zCmd[0]=='/') ){ + if( zCmd && help_is_platform_command(zCmd) ){ + @ Not available in this build: "%h(zCmd)" + }else{ + @ Unknown topic: "%h(zCmd)" + } + }else if( rc==2 ){ + @ Ambiguous prefix: "%h(zCmd)" + }else{ + if( pCmd->zHelp[0]==0 ){ + @ No help available for "%h(pCmd->zName)" + }else if( P("plaintext") ){ + Blob txt; + blob_init(&txt, 0, 0); + help_to_text(pCmd->zHelp, &txt, 0); + @ <pre class="helpPage"> + @ %h(blob_str(&txt)) + @ </pre> + blob_reset(&txt); + }else if( P("raw") ){ + @ <pre class="helpPage"> + @ %h(pCmd->zHelp) + @ </pre> + }else{ + @ <div class="helpPage"> + help_to_html(pCmd->zHelp, cgi_output_blob()); + @ </div> + } + } + }else{ + int i; + const char *zWidth = "28ex"; + unsigned char occHelp[FOSSIL_MX_CMDIDX] = {0}; /* Help str occurrences */ + int bktHelp[FOSSIL_MX_CMDIDX][MX_HELP_DUP] = {{0}};/* Help str->commands */ + style_header("Help"); + search_screen(SRCH_HELP, 0x02); + + @ <a name='commands'></a> + @ <h1>Commands:</h1> + @ <div class="columns" style="column-width: %s(zWidth);"> + @ <ul> + /* Fill in help string buckets */ + for(i=0; i<MX_COMMAND; i++){ + if(aCommand[i].eCmdFlags & CMDFLAG_HIDDEN) continue; + bktHelp[aCommand[i].iHelp][occHelp[aCommand[i].iHelp]++] = i; + } + for(i=0; i<MX_COMMAND; i++){ + const char *z = aCommand[i].zName; + const char *zBoldOn = aCommand[i].eCmdFlags&CMDFLAG_1ST_TIER?"<b>" :""; + const char *zBoldOff = aCommand[i].eCmdFlags&CMDFLAG_1ST_TIER?"</b>":""; + if( '/'==*z || strncmp(z,"test",4)==0 ) continue; + if( (aCommand[i].eCmdFlags & (CMDFLAG_SETTING|CMDFLAG_TOPIC))!=0 ){ + continue; + } + else if( (aCommand[i].eCmdFlags & CMDFLAG_HIDDEN)!=0 ) continue; + else if( (aCommand[i].eCmdFlags & CMDFLAG_ALIAS)!=0 ) continue; + @ <li><a href="%R/help/%s(z)">%s(zBoldOn)%s(z)%s(zBoldOff)</a> + /* Output aliases */ + if( occHelp[aCommand[i].iHelp] > 1 ){ + int j; + int aliases[MX_HELP_DUP], nAliases=0; + for(j=0; j<occHelp[aCommand[i].iHelp]; j++){ + if( bktHelp[aCommand[i].iHelp][j] != i ){ + if( aCommand[bktHelp[aCommand[i].iHelp][j]].eCmdFlags + & CMDFLAG_ALIAS ){ + aliases[nAliases++] = bktHelp[aCommand[i].iHelp][j]; + } + } + } + if( nAliases>0 ){ + int k; + @(\ + for(k=0; k<nAliases; k++){ + @<a href="%R/help/%s(aCommand[aliases[k]].zName)">\ + @%s(aCommand[aliases[k]].zName)</a>%s((k<nAliases-1)?", ":"")\ + } + @)\ + } + } + @ </li> + } + + @ </ul></div> + + @ <a name='webpages'></a> + @ <h1>Web pages:</h1> + @ <div class="columns" style="column-width: %s(zWidth);"> + @ <ul> + for(i=0; i<MX_COMMAND; i++){ + const char *z = aCommand[i].zName; + if( '/'!=*z ) continue; + else if( (aCommand[i].eCmdFlags & CMDFLAG_HIDDEN)!=0 ) continue; + if( aCommand[i].zHelp[0] ){ + @ <li><a href="%R/help/www%s(z)">%s(z+1)</a></li> + }else{ + @ <li>%s(z+1)</li> + } + } + @ </ul></div> + + @ <a name='settings'></a> + @ <h1>Settings:</h1> + @ <div class="columns" style="column-width: %s(zWidth);"> + @ <ul> + for(i=0; i<MX_COMMAND; i++){ + const char *z = aCommand[i].zName; + if( (aCommand[i].eCmdFlags & CMDFLAG_SETTING)==0 ) continue; + else if( (aCommand[i].eCmdFlags & CMDFLAG_HIDDEN)!=0 ) continue; + if( aCommand[i].zHelp[0] ){ + @ <li><a href="%R/help/%s(z)">%s(z)</a></li> + }else{ + @ <li>%s(z)</li> + } + } + @ </ul></div> + + @ <a name='topics'></a> + @ <h1>Other Miscellaneous Help Topics:</h1> + @ <div class="columns" style="column-width: %s(zWidth);"> + @ <ul> + for(i=0; i<MX_COMMAND; i++){ + const char *z = aCommand[i].zName; + if( (aCommand[i].eCmdFlags & CMDFLAG_TOPIC)==0 ) continue; + if( aCommand[i].zHelp[0] ){ + @ <li><a href="%R/help/%s(z)">%s(z)</a></li> + }else{ + @ <li>%s(z)</li> + } + } + @ </ul></div> + + @ <a name='unsupported'></a> + @ <h1>Unsupported and Testing Commands:</h1> + @ <div class="columns" style="column-width: %s(zWidth);"> + @ <ul> + for(i=0; i<MX_COMMAND; i++){ + const char *z = aCommand[i].zName; + if( strncmp(z,"test",4)!=0 ) continue; + else if( (aCommand[i].eCmdFlags & CMDFLAG_HIDDEN)!=0 ) continue; + if( aCommand[i].zHelp[0] ){ + @ <li><a href="%R/help/%s(z)">%s(z)</a></li> + }else{ + @ <li>%s(z)</li> + } + } + @ </ul></div> + + } + style_finish_page(); +} + +/* +** WEBPAGE: test-all-help +** +** Show all help text on a single page. Useful for proof-reading. +*/ +void test_all_help_page(void){ + int i; + unsigned char occHelp[FOSSIL_MX_CMDIDX] = {0}; /* Help string occurrences */ + int bktHelp[FOSSIL_MX_CMDIDX][MX_HELP_DUP] = {{0}};/* Help strings->commands*/ + Blob buf; + blob_init(&buf,0,0); + style_set_current_feature("test"); + style_header("All Help Text"); + @ <dl> + /* Fill in help string buckets */ + for(i=0; i<MX_COMMAND; i++){ + if(aCommand[i].eCmdFlags & CMDFLAG_HIDDEN) continue; + bktHelp[aCommand[i].iHelp][occHelp[aCommand[i].iHelp]++] = i; + } + for(i=0; i<MX_COMMAND; i++){ + const char *zDesc; + unsigned int e = aCommand[i].eCmdFlags; + if( e & CMDFLAG_1ST_TIER ){ + zDesc = "1st tier command"; + }else if( e & CMDFLAG_2ND_TIER ){ + zDesc = "2nd tier command"; + }else if( e & CMDFLAG_ALIAS ){ + zDesc = "alias"; + }else if( e & CMDFLAG_TEST ){ + zDesc = "test command"; + }else if( e & CMDFLAG_TOPIC ){ + zDesc = "help-topic"; + }else if( e & CMDFLAG_WEBPAGE ){ + if( e & CMDFLAG_RAWCONTENT ){ + zDesc = "raw-content web page"; + }else{ + zDesc = "web page"; + } + }else{ + blob_reset(&buf); + if( e & CMDFLAG_VERSIONABLE ){ + blob_appendf(&buf, "versionable "); + } + if( e & CMDFLAG_BLOCKTEXT ){ + blob_appendf(&buf, "block-text "); + } + if( e & CMDFLAG_BOOLEAN ){ + blob_appendf(&buf, "boolean "); + } + blob_appendf(&buf,"setting"); + zDesc = blob_str(&buf); + } + if( memcmp(aCommand[i].zName, "test", 4)==0 ) continue; + if( occHelp[aCommand[i].iHelp] > 0 ){ + int j; + for(j=0; j<occHelp[aCommand[i].iHelp]; j++){ + unsigned int e = aCommand[bktHelp[aCommand[i].iHelp][j]].eCmdFlags; + if( e & CMDFLAG_1ST_TIER ){ + zDesc = "1st tier command"; + }else if( e & CMDFLAG_2ND_TIER ){ + zDesc = "2nd tier command"; + }else if( e & CMDFLAG_ALIAS ){ + zDesc = "alias"; + }else if( e & CMDFLAG_TEST ){ + zDesc = "test command"; + }else if( e & CMDFLAG_WEBPAGE ){ + if( e & CMDFLAG_RAWCONTENT ){ + zDesc = "raw-content web page"; + }else{ + zDesc = "web page"; + } + } + + @ <dt><big><b>%s(aCommand[bktHelp[aCommand[i].iHelp][j]].zName)</b> + @</big> (%s(zDesc))</dt> + } + @ <p><dd> + help_to_html(aCommand[i].zHelp, cgi_output_blob()); + @ </dd><p> + occHelp[aCommand[i].iHelp] = 0; + } + } + @ </dl> + blob_reset(&buf); + style_finish_page(); +} + +/* +** Analyze p and return one of three values: +** +** 0 p is the continuation of a prior subcommand. +** +** 1 p is text past the end of a prior subcommand. +** +** 2 p is the start of a new subcommand. +*/ +static int is_subcommand(Blob *p, int bAbbrevSubcmd){ + int i, sz; + const unsigned char *z = (const unsigned char*)blob_buffer(p); + sz = blob_size(p); + if( sz>6 ) sz = 6; + for(i=0; i<sz && fossil_isspace(z[i]); i++){} + if( i>=sz ) return 0; + + if( bAbbrevSubcmd==0 ){ + if( i>1 ) return 0; + return z[0]=='>' ? 2 : 1; + }else{ + return (i==3 && fossil_isalpha(z[3])) ? 2 : 1; + } +} + +/* +** Input z[] is help text for zTopic. If zTopic has sub-command zSub, +** then cut out all portions of the original help text that do not +** directly pertain to zSub and write the zSub-relevant parts into +** pOut. +** +** Return the number of lines of z[] written into pOut. A return of +** zero means no simplification occurred. +*/ +static int simplify_to_subtopic( + const char *z, /* Full original help text */ + Blob *pOut, /* Write simplified help text here */ + const char *zTopic, /* TOPIC */ + const char *zSubtopic, /* SUBTOPIC */ + int bAbbrevSubcmd /* True if z[] contains abbreviated subcommands */ +){ + Blob in, line;/*, subsection;*/ + int n = 0; + char *zQTop = re_quote(zTopic); + char *zQSub = re_quote(zSubtopic); + char *zPattern; + ReCompiled *pRe = 0; + + if( bAbbrevSubcmd ){ + zPattern = mprintf(" ([a-z]+ ?\\| ?)*%s\\b", zQSub); + }else{ + zPattern = mprintf("> ?fossil [-a-z]+ .*\\b%s\\b", zQSub); + } + fossil_free(zQTop); + fossil_free(zQSub); + fossil_re_compile(&pRe, zPattern, 0); + fossil_free(zPattern); + blob_init(&in, z, -1); + while( blob_line(&in, &line) ){ + if( re_match(pRe, (unsigned char*)blob_buffer(&line), blob_size(&line)) ){ + int atStart = 1; + blob_appendb(pOut, &line); + n++; + while( blob_line(&in, &line) ){ + if( re_match(pRe,(unsigned char*)blob_buffer(&line),blob_size(&line)) ){ + blob_appendb(pOut, &line); + n++; + atStart = 1; + }else{ + int x = is_subcommand(&line,bAbbrevSubcmd); + if( x==2 ){ + if( atStart ){ + blob_appendb(pOut, &line); + n++; + }else{ + break; + } + }else if( x==1 ){ + break; + }else{ + blob_appendb(pOut, &line); + n++; + atStart = 0; + } + } + } + } + } + blob_reset(&line); + re_free(pRe); + if( n ){ + blob_trim(pOut); + blob_reset(&in); + } + return n; +} + +/* +** Input p is a "Usage:" line or a subcommand line. Simplify this line +** for the --usage option and write it into pOut. +*/ +static void simplify_usage_line( + Blob *p, + Blob *pOut, + int bAbbrevSubcmd, + const char *zCmd +){ + const char *z = blob_buffer(p); + int sz = blob_size(p); + int i = 0; + if( sz>6 && z[0]=='U' ){ + for(i=1; i<sz && !fossil_isspace(z[i]); i++){} + }else if( sz>0 && z[0]=='>' ){ + i = 1; + }else if( sz>4 && bAbbrevSubcmd + && memcmp(z," ",3)==0 && !fossil_isspace(z[3]) ){ + int j; + for(j=3; j<sz-1 && (z[j]!=' ' || z[j+1]!=' '); j++){} + blob_appendf(pOut, "fossil %s %.*s\n", zCmd, j-3, &z[3]); + return; + }else{ + while( i<sz && fossil_isspace(z[i]) ) i++; + if( i+2<sz && (z[i]=='o' || z[i]=='O') && z[i+1]=='r' ){ + while( i<sz && !fossil_isspace(z[i]) ) i++; + } + } + while( i<sz && fossil_isspace(z[i]) ) i++; + blob_append(pOut, &z[i], sz-i); +} + +/* +** Input z[] is help text for a command zTopic. Write into pOut all lines of +** z[] that show the command-line syntax for that command. Lines written +** to pOut are lines that begin with out of: +** +** Usage: +** or: +** > fossil TOPIC +** +** Return the number of lines written into pOut. +*/ +static int simplify_to_usage( + const char *z, /* Full original help text */ + Blob *pOut, /* Write simplified help text here */ + const char *zTopic, /* The command for which z[] is full help text */ + int bAbbrevSubcmd /* z[] uses abbreviated subcommands */ +){ + ReCompiled *pRe = 0; + Blob in, line; + int n = 0; + + if( bAbbrevSubcmd ){ + fossil_re_compile(&pRe, "^(Usage: | [a-z][-a-z|]+ .*)", 0); + }else{ + fossil_re_compile(&pRe, "^(Usage: | *[Oo]r: +%fossi |> ?fossil )", 0); + } + blob_init(&in, z, -1); + while( blob_line(&in, &line) ){ + if( re_match(pRe, (unsigned char*)blob_buffer(&line), blob_strlen(&line)) ){ + simplify_usage_line(&line, pOut, bAbbrevSubcmd, zTopic); + n++; + } + } + re_free(pRe); + if( n ) blob_trim(pOut); + return n; +} + +/* +** Input z[] is help text. Write into pOut all lines of z[] that show +** command-line options. Return the number of lines written. +*/ +static int simplify_to_options( + const char *z, /* Full original help text */ + Blob *pOut, /* Write simplified help text here */ + int bAbbrevSubcmd, /* z[] uses abbreviated subcommands */ + const char *zCmd /* Name of the command that z[] describes */ +){ + ReCompiled *pRe = 0; + Blob txt, line, subsection; + int n = 0; + int bSubsectionSeen = 0; + + blob_init(&txt, z, -1); + blob_init(&subsection, 0, 0); + fossil_re_compile(&pRe, "^ +-.* ", 0); + while( blob_line(&txt, &line) ){ + int len = blob_size(&line); + unsigned char *zLine = (unsigned char *)blob_buffer(&line); + if( re_match(pRe, zLine, len) ){ + if( blob_size(&subsection) ){ + simplify_usage_line(&subsection, pOut, bAbbrevSubcmd, zCmd); + blob_reset(&subsection); + } + blob_appendb(pOut, &line); + }else if( len>7 && !fossil_isspace(zLine[0]) && bSubsectionSeen + && sqlite3_strlike("%options:%",blob_str(&line),0)==0 ){ + subsection = line; + }else if( !bAbbrevSubcmd && len>9 + && (memcmp(zLine,"> fossil ",9)==0 + || memcmp(zLine,"> fossil",9)==0) ){ + subsection = line; + bSubsectionSeen = 1; + }else if( bAbbrevSubcmd && len>5 && memcmp(zLine," ",3)==0 + && fossil_isalpha(zLine[3]) ){ + subsection = line; + bSubsectionSeen = 1; + }else if( len>1 && !fossil_isspace(zLine[0]) && bSubsectionSeen ){ + blob_reset(&subsection); + } + } + re_free(pRe); + blob_trim(pOut); + blob_reset(&subsection); + return n; +} + + + +static void multi_column_list(const char **azWord, int nWord){ + int i, j, len; + int mxLen = 0; + int nCol; + int nRow; + for(i=0; i<nWord; i++){ + len = strlen(azWord[i]); + if( len>mxLen ) mxLen = len; + } + nCol = 80/(mxLen+2); + if( nCol==0 ) nCol = 1; + nRow = (nWord + nCol - 1)/nCol; + for(i=0; i<nRow; i++){ + const char *zSpacer = ""; + for(j=i; j<nWord; j+=nRow){ + fossil_print("%s%-*s", zSpacer, mxLen, azWord[j]); + zSpacer = " "; + } + fossil_print("\n"); + } +} + +/* +** COMMAND: test-list-webpage +** +** List all web pages. +*/ +void cmd_test_webpage_list(void){ + int i, nCmd; + const char *aCmd[MX_COMMAND]; + for(i=nCmd=0; i<MX_COMMAND; i++){ + if(CMDFLAG_WEBPAGE & aCommand[i].eCmdFlags){ + aCmd[nCmd++] = aCommand[i].zName; + } + } + assert(nCmd && "page list is empty?"); + multi_column_list(aCmd, nCmd); +} + + +/* +** List of commands starting with zPrefix, or all commands if zPrefix is NULL. +*/ +static void command_list(int cmdMask, int verboseFlag, int useHtml){ + if( verboseFlag ){ + display_all_help(cmdMask, useHtml, 0); + }else{ + int i, nCmd; + const char *aCmd[MX_COMMAND]; + for(i=nCmd=0; i<MX_COMMAND; i++){ + if( (aCommand[i].eCmdFlags & cmdMask)==0 ) continue; + else if(aCommand[i].eCmdFlags & CMDFLAG_HIDDEN) continue; + aCmd[nCmd++] = aCommand[i].zName; + } + multi_column_list(aCmd, nCmd); + } +} + +/* +** TOPIC: options +** +** Command-line options common to all commands: +** +** --args FILENAME Read additional arguments and options from FILENAME +** --case-sensitive BOOL Set case sensitivity for file names +** --cgitrace Active CGI tracing +** --chdir PATH Change to PATH before performing any operations +** --errorlog FILENAME Log errors to FILENAME +** -?|--help Show help on the command rather than running it +** --httptrace Trace outbound HTTP requests +** --localtime Display times using the local timezone +** --nocgi Do not act as CGI +** --no-th-hook Do not run TH1 hooks +** -q|--quiet Reduce the amount of output +** --sqlstats Show SQL usage statistics when done +** --sqltrace Trace all SQL commands +** --sshtrace Trace SSH activity +** --ssl-identity NAME Set the SSL identity to NAME +** --systemtrace Trace calls to system() +** -U|--user USER Make the default user be USER +** --utc Display times using UTC +** --vfs NAME Cause SQLite to use the NAME VFS +** +** Additional options available on most commands that use network I/O: +** +** --accept-any-cert Disable server SSL cdert validation. Accept any SSL +** cert that the server provides. WARNING: Unsafe! +** Testing and debugging use only! +** --ipv4 Use only IPv4. Disable IPv6 support. +** --ipv6 Use only IPv6. Disable IPv4 support. +** --nosync Disable autosync for the current command. +** --proxy URL Specify the HTTP proxy to use. URL can be "off". +*/ + +/* +** COMMAND: help +** +** Usage: %fossil help [OPTIONS] [TOPIC] [SUBCOMMAND] +** +** Display information on how to use TOPIC, which may be a command, webpage, or +** setting. Webpage names begin with "/". If TOPIC is omitted, a list of +** topics is returned. If there is an extra argument after TOPIC, it is +** the name of a subcommand, in which case only the help text for that one +** subcommand is shown. +** +** The following options can be used when TOPIC is omitted: +** +** -a|--all List both common and auxiliary commands +** -e|--everything List all help on all topics +** -f|--full List full set of commands (including auxiliary +** and unsupported "test" commands), options, +** settings, and web pages +** -o|--options List command-line options common to all commands +** -s|--setting List setting names +** -t|--test List unsupported "test" commands +** -v|--verbose List both names and help text +** -x|--aux List only auxiliary commands +** -w|--www List all web pages +** +** These options can be used when TOPIC is present: +** +** -c|--commands Restrict TOPIC search to commands +** -h|--html Format output as HTML rather than plain text +** -o|--options Show command-line options for TOPIC +** --raw Output raw, unformatted help text +** -u|--usage Show a succinct usage summary, not full help text +** +** See also: [[usage]], [[options]], [[search]] with the -h option +*/ +void help_cmd(void){ + int rc; + int mask = CMDFLAG_ANY; /* Mask of help topic types */ + int isPage = 0; /* True if TOPIC is a page */ + int verboseFlag = 0; /* -v option */ + int commandsFlag = 0; /* -c option */ + const char *z; /* Original, untranslated help text */ + const char *zCmdOrPage; /* "command" or "page" or "setting" */ + const CmdOrPage *pCmd = 0; /* ptr to aCommand[] entry for TOPIC */ + int useHtml = 0; /* -h option */ + int bUsage; /* --usage */ + int bRaw; /* --raw option */ + int bOptions; /* --options */ + const char *zTopic; /* TOPIC argument */ + const char *zSubtopic = 0; /* SUBTOPIC argument */ + Blob subtext1, subtext2, s3; /* Subsets of z[] containing subtopic/usage */ + Blob txt; /* Text after rendering */ + int bAbbrevSubcmd = 0; /* Help text uses abbreviated subcommands */ + + verboseFlag = find_option("verbose","v",0)!=0; + commandsFlag = find_option("commands","c",0)!=0; + useHtml = find_option("html","h",0)!=0; + bRaw = find_option("raw",0,0)!=0; + bOptions = find_option("options","o",0)!=0; + bUsage = find_option("usage","u",0)!=0; + if( find_option("all","a",0) ){ + command_list(CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER, verboseFlag, useHtml); + return; + } + else if( find_option("www","w",0) ){ + command_list(CMDFLAG_WEBPAGE, verboseFlag, useHtml); + return; + } + else if( find_option("aux","x",0) ){ + command_list(CMDFLAG_2ND_TIER, verboseFlag, useHtml); + return; + } + else if( find_option("test","t",0) ){ + command_list(CMDFLAG_TEST, verboseFlag, useHtml); + return; + } + else if( find_option("setting","s",0) ){ + command_list(CMDFLAG_SETTING, verboseFlag, useHtml); + return; + } + else if( find_option("topic","c",0) ){ + command_list(CMDFLAG_TOPIC, verboseFlag, useHtml); + return; + } + else if( find_option("full","f",0) ){ + fossil_print("fossil commands:\n\n"); + command_list(CMDFLAG_1ST_TIER, verboseFlag, useHtml); + fossil_print("\nfossil auxiliary commands:\n\n"); + command_list(CMDFLAG_2ND_TIER, verboseFlag, useHtml); + fossil_print("\nfossil settings:\n\n"); + command_list(CMDFLAG_SETTING, verboseFlag, useHtml); + fossil_print("\nfossil web pages:\n\n"); + command_list(CMDFLAG_WEBPAGE, verboseFlag, useHtml); + fossil_print("\nfossil miscellaneous help topics:\n\n"); + command_list(CMDFLAG_TOPIC, verboseFlag, useHtml); + fossil_print("\nfossil test commands (unsupported):\n\n"); + command_list(CMDFLAG_TEST, verboseFlag, useHtml); + if ( !verboseFlag ) { + fossil_print("\n"); + version_cmd(); + } + return; + } + else if( find_option("everything","e",0) ){ + display_all_help(CMDFLAG_1ST_TIER | CMDFLAG_2ND_TIER | CMDFLAG_WEBPAGE | + CMDFLAG_SETTING | CMDFLAG_TEST | CMDFLAG_TOPIC, + useHtml, 0); + return; + } + verify_all_options(); + zCmdOrPage = "help topic"; + if( g.argc<3 ){ + if( bOptions ){ + zTopic = "options"; + zSubtopic = 0; + mask = CMDFLAG_TOPIC; + bOptions = 0; + goto find_and_show_help; + } + z = g.argv[0]; + fossil_print( + "Usage: %s help TOPIC\n" + "Things to try:\n\n" + " %s help help\n" + " %s help -o\n" + " %s help -a\n" + " %s search -h TOPIC\n\n" + "Other common values for TOPIC:\n\n", + z, z, z, z, z); + command_list(CMDFLAG_1ST_TIER,verboseFlag,useHtml); + if( !verboseFlag ) version_cmd(); + return; + } + zTopic = g.argv[2]; + zSubtopic = g.argc>=4 ? g.argv[3] : 0; + isPage = ('/' == zTopic[0]) ? 1 : 0; + if( isPage ){ + zCmdOrPage = "page"; + }else if( commandsFlag ){ + mask = CMDFLAG_COMMAND; + zCmdOrPage = "command"; + } +find_and_show_help: + rc = dispatch_name_search(zTopic, mask|CMDFLAG_PREFIX, &pCmd); + if( rc ){ + int i, n; + const char *az[5]; + if( rc==1 ){ + if( help_is_platform_command(g.argv[2]) ){ + fossil_print("Not available in this build: %s\n", g.argv[2]); + return; + } + fossil_print("unknown %s: %s\n", zCmdOrPage, g.argv[2]); + }else{ + fossil_print("ambiguous %s prefix: %s\n", + zCmdOrPage, g.argv[2]); + } + fossil_print("Did you mean one of these TOPICs:\n"); + n = dispatch_approx_match(g.argv[2], 5, az); + for(i=0; i<n; i++){ + fossil_print(" * %s\n", az[i]); + } + fossil_print("Other commands to try:\n"); + fossil_print(" fossil search -h PATTERN ;# search all help text\n"); + fossil_print(" fossil help -a ;# show all commands\n"); + fossil_print(" fossil help -w ;# show all web-pages\n"); + fossil_print(" fossil help -s ;# show all settings\n"); + fossil_print(" fossil help -o ;# show global options\n"); + return; + } + bAbbrevSubcmd = (pCmd->eCmdFlags & CMDFLAG_ABBREVSUBCMD)!=0; + z = pCmd->zHelp; + if( z==0 ){ + fossil_fatal("no help available for the %s %s", + pCmd->zName, zCmdOrPage); + } + blob_init(&subtext1, 0, 0); + blob_init(&subtext2, 0, 0); + blob_init(&s3, 0, 0); + if( zSubtopic!=0 ){ + if( simplify_to_subtopic(z, &subtext1, zTopic, zSubtopic, bAbbrevSubcmd) ){ + z = blob_str(&subtext1); + }else{ + fossil_print("No subtopic \"%s\" for \"%s\".\n", zSubtopic, zTopic); + bUsage = 1; + zSubtopic = 0; + } + } + if( bUsage ){ + if( simplify_to_usage(z, &subtext2, zTopic, bAbbrevSubcmd) ){ + z = blob_str(&subtext2); + }else{ + bUsage = 0; + } + } + if( bOptions ){ + simplify_to_options(z, &s3, bAbbrevSubcmd, zTopic); + z = blob_str(&s3); + } + if( pCmd && pCmd->eCmdFlags & CMDFLAG_SETTING ){ + const Setting *pSetting = db_find_setting(pCmd->zName, 0); + char *zDflt = 0; + if( pSetting!=0 && pSetting->def!=0 && *pSetting->def!=0 ){ + zDflt = mprintf(" (default: %s)", pSetting->def); + } + fossil_print("Setting: \"%s\"%s%s\n\n", + pCmd->zName, zDflt!=0 ? zDflt : "", + (pCmd->eCmdFlags & CMDFLAG_VERSIONABLE)!=0 ? " (versionable)" : "" + ); + fossil_free(zDflt); + } + blob_init(&txt, 0, 0); + if( bRaw ){ + blob_append(&txt, z, -1); + }else if( useHtml ){ + help_to_html(z, &txt); + }else{ + help_to_text(z, &txt, bUsage || zSubtopic!=0); + } + if( blob_strlen(&txt)>0 ) fossil_print("%s\n", blob_str(&txt)); + blob_reset(&txt); + blob_reset(&subtext1); + blob_reset(&subtext2); +} + +/* +** Return a pointer to the setting information array. +** +** This routine provides access to the aSetting[] array which is created +** by the mkindex utility program and included with <page_index.h>. +*/ +const Setting *setting_info(int *pnCount){ + if( pnCount ) *pnCount = (int)(sizeof(aSetting)/sizeof(aSetting[0])) - 1; + return aSetting; +} + +/* +** Return a pointer to a specific Setting entry for the setting named +** in the argument. Or return NULL if no such setting exists. +** +** The pointer returned points into the middle of the global aSetting[] +** array that is generated by mkindex. Use setting_info() to fetch the +** whole array. Use this routine to fetch a specific entry. +*/ +const Setting *setting_find(const char *zName){ + int iFirst = 0; + int iLast = ArraySize(aSetting)-1; + while( iFirst<=iLast ){ + int iCur = (iFirst+iLast)/2; + int c = strcmp(aSetting[iCur].name, zName); + if( c<0 ){ + iFirst = iCur+1; + }else if( c>0 ){ + iLast = iCur-1; + }else{ + return &aSetting[iCur]; + } + } + return 0; +} + +/***************************************************************************** +** A virtual table for accessing the information in aCommand[], and +** especially the help-text +*/ + +/* helptextVtab_vtab is a subclass of sqlite3_vtab which is +** underlying representation of the virtual table +*/ +typedef struct helptextVtab_vtab helptextVtab_vtab; +struct helptextVtab_vtab { + sqlite3_vtab base; /* Base class - must be first */ + /* Add new fields here, as necessary */ +}; + +/* helptextVtab_cursor is a subclass of sqlite3_vtab_cursor which will +** serve as the underlying representation of a cursor that scans +** over rows of the result +*/ +typedef struct helptextVtab_cursor helptextVtab_cursor; +struct helptextVtab_cursor { + sqlite3_vtab_cursor base; /* Base class - must be first */ + /* Insert new fields here. For this helptextVtab we only keep track + ** of the rowid */ + sqlite3_int64 iRowid; /* The rowid */ +}; + +/* +** The helptextVtabConnect() method is invoked to create a new +** helptext virtual table. +** +** Think of this routine as the constructor for helptextVtab_vtab objects. +** +** All this routine needs to do is: +** +** (1) Allocate the helptextVtab_vtab object and initialize all fields. +** +** (2) Tell SQLite (via the sqlite3_declare_vtab() interface) what the +** result set of queries against the virtual table will look like. +*/ +static int helptextVtabConnect( + sqlite3 *db, + void *pAux, + int argc, const char *const*argv, + sqlite3_vtab **ppVtab, + char **pzErr +){ + helptextVtab_vtab *pNew; + int rc; + + rc = sqlite3_declare_vtab(db, + "CREATE TABLE x(name,type,flags,helptext,formatted,html)" + ); + if( rc==SQLITE_OK ){ + pNew = sqlite3_malloc( sizeof(*pNew) ); + *ppVtab = (sqlite3_vtab*)pNew; + if( pNew==0 ) return SQLITE_NOMEM; + memset(pNew, 0, sizeof(*pNew)); + } + return rc; +} + +/* +** This method is the destructor for helptextVtab_vtab objects. +*/ +static int helptextVtabDisconnect(sqlite3_vtab *pVtab){ + helptextVtab_vtab *p = (helptextVtab_vtab*)pVtab; + sqlite3_free(p); + return SQLITE_OK; +} + +/* +** Constructor for a new helptextVtab_cursor object. +*/ +static int helptextVtabOpen(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor){ + helptextVtab_cursor *pCur; + pCur = sqlite3_malloc( sizeof(*pCur) ); + if( pCur==0 ) return SQLITE_NOMEM; + memset(pCur, 0, sizeof(*pCur)); + *ppCursor = &pCur->base; + return SQLITE_OK; +} + +/* +** Destructor for a helptextVtab_cursor. +*/ +static int helptextVtabClose(sqlite3_vtab_cursor *cur){ + helptextVtab_cursor *pCur = (helptextVtab_cursor*)cur; + sqlite3_free(pCur); + return SQLITE_OK; +} + + +/* +** Advance a helptextVtab_cursor to its next row of output. +*/ +static int helptextVtabNext(sqlite3_vtab_cursor *cur){ + helptextVtab_cursor *pCur = (helptextVtab_cursor*)cur; + pCur->iRowid++; + return SQLITE_OK; +} + +/* +** Return values of columns for the row at which the helptextVtab_cursor +** is currently pointing. +*/ +static int helptextVtabColumn( + sqlite3_vtab_cursor *cur, /* The cursor */ + sqlite3_context *ctx, /* First argument to sqlite3_result_...() */ + int i /* Which column to return */ +){ + helptextVtab_cursor *pCur = (helptextVtab_cursor*)cur; + const CmdOrPage *pPage = aCommand + pCur->iRowid; + switch( i ){ + case 0: /* name */ + sqlite3_result_text(ctx, pPage->zName, -1, SQLITE_STATIC); + break; + case 1: { /* type */ + const char *zType = 0; + if( pPage->eCmdFlags & CMDFLAG_COMMAND ){ + zType = "command"; + }else if( pPage->eCmdFlags & CMDFLAG_WEBPAGE ){ + zType = "webpage"; + }else if( pPage->eCmdFlags & CMDFLAG_SETTING ){ + zType = "setting"; + }else if( pPage->eCmdFlags & CMDFLAG_TOPIC ){ + zType = "help-topic"; + } + sqlite3_result_text(ctx, zType, -1, SQLITE_STATIC); + break; + } + case 2: /* flags */ + sqlite3_result_int(ctx, pPage->eCmdFlags); + break; + case 3: /* helptext */ + sqlite3_result_text(ctx, pPage->zHelp, -1, SQLITE_STATIC); + break; + case 4: { /* formatted */ + Blob txt; + blob_init(&txt, 0, 0); + help_to_text(pPage->zHelp, &txt, 0); + sqlite3_result_text(ctx, blob_str(&txt), -1, fossil_free); + break; + } + case 5: { /* formatted */ + Blob txt; + blob_init(&txt, 0, 0); + help_to_html(pPage->zHelp, &txt); + sqlite3_result_text(ctx, blob_str(&txt), -1, fossil_free); + break; + } + } + return SQLITE_OK; +} + +/* +** Return the rowid for the current row. In this implementation, the +** rowid is the same as the output value. +*/ +static int helptextVtabRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ + helptextVtab_cursor *pCur = (helptextVtab_cursor*)cur; + *pRowid = pCur->iRowid; + return SQLITE_OK; +} + +/* +** Return TRUE if the cursor has been moved off of the last +** row of output. +*/ +static int helptextVtabEof(sqlite3_vtab_cursor *cur){ + helptextVtab_cursor *pCur = (helptextVtab_cursor*)cur; + return pCur->iRowid>=MX_COMMAND; +} + +/* +** This method is called to "rewind" the helptextVtab_cursor object back +** to the first row of output. This method is always called at least +** once prior to any call to helptextVtabColumn() or helptextVtabRowid() or +** helptextVtabEof(). +*/ +static int helptextVtabFilter( + sqlite3_vtab_cursor *pVtabCursor, + int idxNum, const char *idxStr, + int argc, sqlite3_value **argv +){ + helptextVtab_cursor *pCur = (helptextVtab_cursor *)pVtabCursor; + pCur->iRowid = 1; + return SQLITE_OK; +} + +/* +** SQLite will invoke this method one or more times while planning a query +** that uses the virtual table. This routine needs to create +** a query plan for each invocation and compute an estimated cost for that +** plan. +*/ +static int helptextVtabBestIndex( + sqlite3_vtab *tab, + sqlite3_index_info *pIdxInfo +){ + pIdxInfo->estimatedCost = (double)MX_COMMAND; + pIdxInfo->estimatedRows = MX_COMMAND; + return SQLITE_OK; +} + +/* +** This following structure defines all the methods for the +** virtual table. +*/ +static sqlite3_module helptextVtabModule = { + /* iVersion */ 0, + /* xCreate */ 0, /* Helptext is eponymous and read-only */ + /* xConnect */ helptextVtabConnect, + /* xBestIndex */ helptextVtabBestIndex, + /* xDisconnect */ helptextVtabDisconnect, + /* xDestroy */ 0, + /* xOpen */ helptextVtabOpen, + /* xClose */ helptextVtabClose, + /* xFilter */ helptextVtabFilter, + /* xNext */ helptextVtabNext, + /* xEof */ helptextVtabEof, + /* xColumn */ helptextVtabColumn, + /* xRowid */ helptextVtabRowid, + /* xUpdate */ 0, + /* xBegin */ 0, + /* xSync */ 0, + /* xCommit */ 0, + /* xRollback */ 0, + /* xFindMethod */ 0, + /* xRename */ 0, + /* xSavepoint */ 0, + /* xRelease */ 0, + /* xRollbackTo */ 0, + /* xShadowName */ 0, + /* xIntegrity */ 0 +}; + + +/* +** Register the helptext virtual table +*/ +int helptext_vtab_register(sqlite3 *db){ + int rc = sqlite3_create_module(db, "helptext", &helptextVtabModule, 0); + return rc; +} +/* End of the helptext virtual table +******************************************************************************/ diff --git a/src/doc.c b/src/doc.c new file mode 100644 index 0000000..35e0709 --- /dev/null +++ b/src/doc.c @@ -0,0 +1,1277 @@ +/* +** Copyright (c) 2007 D. Richard Hipp +** +** This program is free software; you can redistribute it and/or +** modify it under the terms of the Simplified BSD License (also +** known as the "2-Clause License" or "FreeBSD License".) +** +** This program is distributed in the hope that it will be useful, +** but without any warranty; without even the implied warranty of +** merchantability or fitness for a particular purpose. +** +** Author contact information: +** drh@hwaci.com +** http://www.hwaci.com/drh/ +** +******************************************************************************* +** +** This file contains code to implement the "/doc" web page and related +** pages. +*/ +#include "config.h" +#include "doc.h" +#include <assert.h> + +/* +** Try to guess the mimetype from content. +** +** If the content is pure text, return NULL. +** +** For image types, attempt to return an appropriate mimetype +** name like "image/gif" or "image/jpeg". +** +** For any other binary type, return "unknown/unknown". +*/ +const char *mimetype_from_content(Blob *pBlob){ + int i; + int n; + const unsigned char *x; + + /* A table of mimetypes based on file content prefixes + */ + static const struct { + const char *z; /* Identifying file text */ + const unsigned char sz1; /* Length of the prefix */ + const unsigned char of2; /* Offset to the second segment */ + const unsigned char sz2; /* Size of the second segment */ + const unsigned char mn; /* Minimum size of input */ + const char *zMimetype; /* The corresponding mimetype */ + } aMime[] = { + { "GIF87a", 6, 0, 0, 6, "image/gif" }, + { "GIF89a", 6, 0, 0, 6, "image/gif" }, + { "\211PNG\r\n\032\n", 8, 0, 0, 8, "image/png" }, + { "\377\332\377", 3, 0, 0, 3, "image/jpeg" }, + { "\377\330\377", 3, 0, 0, 3, "image/jpeg" }, + { "RIFFWAVEfmt", 4, 8, 7, 15, "sound/wav" }, + }; + + if( !looks_like_binary(pBlob) ) { + return 0; /* Plain text */ + } + x = (const unsigned char*)blob_buffer(pBlob); + n = blob_size(pBlob); + for(i=0; i<count(aMime); i++){ + if( n<aMime[i].mn ) continue; + if( memcmp(x, aMime[i].z, aMime[i].sz1)!=0 ) continue; + if( aMime[i].sz2 + && memcmp(x+aMime[i].of2, aMime[i].z+aMime[i].sz1, aMime[i].sz2)!=0 + ){ + continue; + } + return aMime[i].zMimetype; + } + return "unknown/unknown"; +} + +/* A table of mimetypes based on file suffixes. +** Suffixes must be in sorted order so that we can do a binary +** search to find the mimetype. +*/ +static const struct { + const char *zSuffix; /* The file suffix */ + int size; /* Length of the suffix */ + const char *zMimetype; /* The corresponding mimetype */ +} aMime[] = { + { "ai", 2, "application/postscript" }, + { "aif", 3, "audio/x-aiff" }, + { "aifc", 4, "audio/x-aiff" }, + { "aiff", 4, "audio/x-aiff" }, + { "arj", 3, "application/x-arj-compressed" }, + { "asc", 3, "text/plain" }, + { "asf", 3, "video/x-ms-asf" }, + { "asx", 3, "video/x-ms-asx" }, + { "au", 2, "audio/ulaw" }, + { "avi", 3, "video/x-msvideo" }, + { "bat", 3, "application/x-msdos-program" }, + { "bcpio", 5, "application/x-bcpio" }, + { "bin", 3, "application/octet-stream" }, + { "bmp", 3, "image/bmp" }, + { "bz2", 3, "application/x-bzip2" }, + { "bzip", 4, "application/x-bzip" }, + { "c", 1, "text/plain" }, + { "cc", 2, "text/plain" }, + { "ccad", 4, "application/clariscad" }, + { "cdf", 3, "application/x-netcdf" }, + { "class", 5, "application/octet-stream" }, + { "cod", 3, "application/vnd.rim.cod" }, + { "com", 3, "application/x-msdos-program" }, + { "cpio", 4, "application/x-cpio" }, + { "cpt", 3, "application/mac-compactpro" }, + { "cs", 2, "text/plain" }, + { "csh", 3, "application/x-csh" }, + { "css", 3, "text/css" }, + { "csv", 3, "text/csv" }, + { "dcr", 3, "application/x-director" }, + { "deb", 3, "application/x-debian-package" }, + { "dib", 3, "image/bmp" }, + { "dir", 3, "application/x-director" }, + { "dl", 2, "video/dl" }, + { "dms", 3, "application/octet-stream" }, + { "doc", 3, "application/msword" }, + { "docx", 4, "application/vnd.openxmlformats-" + "officedocument.wordprocessingml.document"}, + { "dot", 3, "application/msword" }, + { "dotx", 4, "application/vnd.openxmlformats-" + "officedocument.wordprocessingml.template"}, + { "drw", 3, "application/drafting" }, + { "dvi", 3, "application/x-dvi" }, + { "dwg", 3, "application/acad" }, + { "dxf", 3, "application/dxf" }, + { "dxr", 3, "application/x-director" }, + { "eps", 3, "application/postscript" }, + { "etx", 3, "text/x-setext" }, + { "exe", 3, "application/octet-stream" }, + { "ez", 2, "application/andrew-inset" }, + { "f", 1, "text/plain" }, + { "f90", 3, "text/plain" }, + { "fli", 3, "video/fli" }, + { "flv", 3, "video/flv" }, + { "gif", 3, "image/gif" }, + { "gl", 2, "video/gl" }, + { "gtar", 4, "application/x-gtar" }, + { "gz", 2, "application/x-gzip" }, + { "h", 1, "text/plain" }, + { "hdf", 3, "application/x-hdf" }, + { "hh", 2, "text/plain" }, + { "hqx", 3, "application/mac-binhex40" }, + { "htm", 3, "text/html" }, + { "html", 4, "text/html" }, + { "ice", 3, "x-conference/x-cooltalk" }, + { "ico", 3, "image/vnd.microsoft.icon" }, + { "ief", 3, "image/ief" }, + { "iges", 4, "model/iges" }, + { "igs", 3, "model/iges" }, + { "ips", 3, "application/x-ipscript" }, + { "ipx", 3, "application/x-ipix" }, + { "jad", 3, "text/vnd.sun.j2me.app-descriptor" }, + { "jar", 3, "application/java-archive" }, + { "jp2", 3, "image/jp2" }, + { "jpe", 3, "image/jpeg" }, + { "jpeg", 4, "image/jpeg" }, + { "jpg", 3, "image/jpeg" }, + { "js", 2, "text/javascript" }, + /* application/javascript is commonly used for JS, but the + ** spec says text/javascript is correct: + ** https://html.spec.whatwg.org/multipage/scripting.html + ** #scriptingLanguages:javascript-mime-type */ + { "json", 4, "application/json" }, + { "kar", 3, "audio/midi" }, + { "latex", 5, "application/x-latex" }, + { "lha", 3, "application/octet-stream" }, + { "lsp", 3, "application/x-lisp" }, + { "lzh", 3, "application/octet-stream" }, + { "m", 1, "text/plain" }, + { "m3u", 3, "audio/x-mpegurl" }, + { "m4a", 3, "audio/mp4" + /* Many sources call this audio/m4a, but that's + deprecated */ }, + { "man", 3, "text/plain" }, + { "markdown", 8, "text/x-markdown" }, + { "md", 2, "text/x-markdown" }, + { "me", 2, "application/x-troff-me" }, + { "mesh", 4, "model/mesh" }, + { "mid", 3, "audio/midi" }, + { "midi", 4, "audio/midi" }, + { "mif", 3, "application/x-mif" }, + { "mime", 4, "www/mime" }, + { "mjs", 3, "text/javascript" /*ES6 module*/ }, + { "mkd", 3, "text/x-markdown" }, + { "mov", 3, "video/quicktime" }, + { "movie", 5, "video/x-sgi-movie" }, + { "mp2", 3, "audio/mpeg" }, + { "mp3", 3, "audio/mpeg" }, + { "mp4", 3, "video/mp4" }, + { "mpe", 3, "video/mpeg" }, + { "mpeg", 4, "video/mpeg" }, + { "mpg", 3, "video/mpeg" }, + { "mpga", 4, "audio/mpeg" }, + { "ms", 2, "application/x-troff-ms" }, + { "msh", 3, "model/mesh" }, + { "n", 1, "text/plain" }, + { "nc", 2, "application/x-netcdf" }, + { "oda", 3, "application/oda" }, + { "odp", 3, "application/vnd.oasis.opendocument.presentation" }, + { "ods", 3, "application/vnd.oasis.opendocument.spreadsheet" }, + { "odt", 3, "application/vnd.oasis.opendocument.text" }, + { "ogg", 3, "application/ogg" }, + { "ogm", 3, "application/ogg" }, + { "org", 3, "text/x-org" }, + { "otf", 3, "font/otf" }, + { "pbm", 3, "image/x-portable-bitmap" }, + { "pdb", 3, "chemical/x-pdb" }, + { "pdf", 3, "application/pdf" }, + { "pgm", 3, "image/x-portable-graymap" }, + { "pgn", 3, "application/x-chess-pgn" }, + { "pgp", 3, "application/pgp" }, + { "pikchr", 6, "text/x-pikchr" }, + { "pl", 2, "application/x-perl" }, + { "pm", 2, "application/x-perl" }, + { "png", 3, "image/png" }, + { "pnm", 3, "image/x-portable-anymap" }, + { "pot", 3, "application/mspowerpoint" }, + { "potx", 4, "application/vnd.openxmlformats-" + "officedocument.presentationml.template"}, + { "ppm", 3, "image/x-portable-pixmap" }, + { "pps", 3, "application/mspowerpoint" }, + { "ppsx", 4, "application/vnd.openxmlformats-" + "officedocument.presentationml.slideshow"}, + { "ppt", 3, "application/mspowerpoint" }, + { "pptx", 4, "application/vnd.openxmlformats-" + "officedocument.presentationml.presentation"}, + { "ppz", 3, "application/mspowerpoint" }, + { "pre", 3, "application/x-freelance" }, + { "prt", 3, "application/pro_eng" }, + { "ps", 2, "application/postscript" }, + { "qt", 2, "video/quicktime" }, + { "ra", 2, "audio/x-realaudio" }, + { "ram", 3, "audio/x-pn-realaudio" }, + { "rar", 3, "application/x-rar-compressed" }, + { "ras", 3, "image/cmu-raster" }, + { "rgb", 3, "image/x-rgb" }, + { "rm", 2, "audio/x-pn-realaudio" }, + { "roff", 4, "application/x-troff" }, + { "rpm", 3, "audio/x-pn-realaudio-plugin" }, + { "rtf", 3, "text/rtf" }, + { "rtx", 3, "text/richtext" }, + { "scm", 3, "application/x-lotusscreencam" }, + { "set", 3, "application/set" }, + { "sgm", 3, "text/sgml" }, + { "sgml", 4, "text/sgml" }, + { "sh", 2, "application/x-sh" }, + { "shar", 4, "application/x-shar" }, + { "silo", 4, "model/mesh" }, + { "sit", 3, "application/x-stuffit" }, + { "skd", 3, "application/x-koan" }, + { "skm", 3, "application/x-koan" }, + { "skp", 3, "application/x-koan" }, + { "skt", 3, "application/x-koan" }, + { "smi", 3, "application/smil" }, + { "smil", 4, "application/smil" }, + { "snd", 3, "audio/basic" }, + { "sol", 3, "application/solids" }, + { "spl", 3, "application/x-futuresplash" }, + { "sql", 3, "application/sql" }, + { "src", 3, "application/x-wais-source" }, + { "step", 4, "application/STEP" }, + { "stl", 3, "application/SLA" }, + { "stp", 3, "application/STEP" }, + { "sv4cpio", 7, "application/x-sv4cpio" }, + { "sv4crc", 6, "application/x-sv4crc" }, + { "svg", 3, "image/svg+xml" }, + { "swf", 3, "application/x-shockwave-flash" }, + { "t", 1, "application/x-troff" }, + { "tar", 3, "application/x-tar" }, + { "tcl", 3, "application/x-tcl" }, + { "tex", 3, "application/x-tex" }, + { "texi", 4, "application/x-texinfo" }, + { "texinfo", 7, "application/x-texinfo" }, + { "tgz", 3, "application/x-tar-gz" }, + { "th1", 3, "application/x-th1" }, + { "tif", 3, "image/tiff" }, + { "tiff", 4, "image/tiff" }, + { "tr", 2, "application/x-troff" }, + { "tsi", 3, "audio/TSP-audio" }, + { "tsp", 3, "application/dsptype" }, + { "tsv", 3, "text/tab-separated-values" }, + { "txt", 3, "text/plain" }, + { "unv", 3, "application/i-deas" }, + { "ustar", 5, "application/x-ustar" }, + { "vb", 2, "text/plain" }, + { "vcd", 3, "application/x-cdlink" }, + { "vda", 3, "application/vda" }, + { "viv", 3, "video/vnd.vivo" }, + { "vivo", 4, "video/vnd.vivo" }, + { "vrml", 4, "model/vrml" }, + { "wasm", 4, "application/wasm" }, + { "wav", 3, "audio/x-wav" }, + { "wax", 3, "audio/x-ms-wax" }, + { "webp", 4, "image/webp" }, + { "wiki", 4, "text/x-fossil-wiki" }, + { "wma", 3, "audio/x-ms-wma" }, + { "wmv", 3, "video/x-ms-wmv" }, + { "wmx", 3, "video/x-ms-wmx" }, + { "wrl", 3, "model/vrml" }, + { "wvx", 3, "video/x-ms-wvx" }, + { "xbm", 3, "image/x-xbitmap" }, + { "xlc", 3, "application/vnd.ms-excel" }, + { "xll", 3, "application/vnd.ms-excel" }, + { "xlm", 3, "application/vnd.ms-excel" }, + { "xls", 3, "application/vnd.ms-excel" }, + { "xlsx", 4, "application/vnd.openxmlformats-" + "officedocument.spreadsheetml.sheet"}, + { "xlw", 3, "application/vnd.ms-excel" }, + { "xml", 3, "text/xml" }, + { "xpm", 3, "image/x-xpixmap" }, + { "xsl", 3, "text/xml" }, + { "xslt", 4, "text/xml" }, + { "xwd", 3, "image/x-xwindowdump" }, + { "xyz", 3, "chemical/x-pdb" }, + { "zip", 3, "application/zip" }, +}; + +/* +** Verify that all entries in the aMime[] table are in sorted order. +** Abort with a fatal error if any is out-of-order. +*/ +static void mimetype_verify(void){ + int i; + for(i=1; i<count(aMime); i++){ + if( fossil_strcmp(aMime[i-1].zSuffix,aMime[i].zSuffix)>=0 ){ + fossil_panic("mimetypes out of sequence: %s before %s", + aMime[i-1].zSuffix, aMime[i].zSuffix); + } + } +} + +/* +** Looks in the contents of the "mimetypes" setting for a suffix +** matching zSuffix. If found, it returns the configured value +** in memory owned by the app (i.e. do not free() it), else it +** returns 0. +** +** The mimetypes setting is expected to be a list of file extensions +** and mimetypes, with one such mapping per line. A leading '.' on +** extensions is permitted for compatibility with lists imported from +** other tools which require them. +*/ +static const char *mimetype_from_name_custom(const char *zSuffix){ + static char * zList = 0; + static char const * zEnd = 0; + static int once = 0; + char * z; + int tokenizerState /* 0=expecting a key, 1=skip next token, + ** 2=accept next token */; + if(once==0){ + once = 1; + zList = db_get("mimetypes",0); + if(zList==0){ + return 0; + } + /* Transform zList to simplify the main loop: + replace non-newline spaces with NUL bytes. */ + zEnd = zList + strlen(zList); + for(z = zList; z<zEnd; ++z){ + if('\n'==*z) continue; + else if(fossil_isspace(*z)){ + *z = 0; + } + } + }else if(zList==0){ + return 0; + } + tokenizerState = 0; + z = zList; + while( z<zEnd ){ + if(*z==0){ + ++z; + continue; + } + else if('\n'==*z){ + if(2==tokenizerState){ + /* We were expecting a value for a successful match + here, but got no value. Bail out. */ + break; + }else{ + /* May happen on malformed inputs. Skip this record. */ + tokenizerState = 0; + ++z; + continue; + } + } + switch(tokenizerState){ + case 0:{ /* This is a file extension */ + static char * zCase = 0; + if('.'==*z){ + /*ignore an optional leading dot, for compatibility + with some external mimetype lists*/; + if(++z==zEnd){ + break; + } + } + if(zCase<z){ + /*we have not yet case-folded this section: lower-case it*/ + for(zCase = z; zCase<zEnd && *zCase!=0; ++zCase){ + if(!(0x80 & *zCase)){ + *zCase = (char)fossil_tolower(*zCase); + } + } + } + if(strcmp(z,zSuffix)==0){ + tokenizerState = 2 /* Match: accept the next value. */; + }else{ + tokenizerState = 1 /* No match: skip the next value. */; + } + z += strlen(z); + break; + } + case 1: /* This is a value, but not a match. Skip it. */ + z += strlen(z); + break; + case 2: /* This is the value which matched the previous key. */; + return z; + default: + assert(!"cannot happen - invalid tokenizerState value."); + } + } + return 0; +} + +/* +** Emit Javascript which applies (or optionally can apply) to both the +** /doc and /wiki pages. None of this implements required +** functionality, just nice-to-haves. Any calls after the first are +** no-ops. +*/ +void document_emit_js(void){ + static int once = 0; + if(0==once++){ + builtin_fossil_js_bundle_or("pikchr", NULL); + style_script_begin(__FILE__,__LINE__); + CX("window.addEventListener('load', " + "()=>window.fossil.pikchr.addSrcView(), " + "false);\n"); + style_script_end(); + } +} + +/* +** Guess the mimetype of a document based on its name. +*/ +const char *mimetype_from_name(const char *zName){ + const char *z; + int i; + int first, last; + int len; + char zSuffix[20]; + + +#ifdef FOSSIL_DEBUG + /* This is test code to make sure the table above is in the correct + ** order + */ + if( fossil_strcmp(zName, "mimetype-test")==0 ){ + mimetype_verify(); + return "ok"; + } +#endif + + z = zName; + for(i=0; zName[i]; i++){ + if( zName[i]=='.' ) z = &zName[i+1]; + } + len = strlen(z); + if( len<(int)sizeof(zSuffix)-1 ){ + sqlite3_snprintf(sizeof(zSuffix), zSuffix, "%s", z); + for(i=0; zSuffix[i]; i++) zSuffix[i] = fossil_tolower(zSuffix[i]); + z = mimetype_from_name_custom(zSuffix); + if(z!=0){ + return z; + } + first = 0; + last = count(aMime) - 1; + while( first<=last ){ + int c; + i = (first+last)/2; + c = fossil_strcmp(zSuffix, aMime[i].zSuffix); + if( c==0 ) return aMime[i].zMimetype; + if( c<0 ){ + last = i-1; + }else{ + first = i+1; + } + } + } + return "application/x-fossil-artifact"; +} + +/* +** COMMAND: test-mimetype +** +** Usage: %fossil test-mimetype FILENAME... +** +** Return the deduced mimetype for each file listed. +** +** If Fossil is compiled with -DFOSSIL_DEBUG then the "mimetype-test" +** filename is special and verifies the integrity of the mimetype table. +** It should return "ok". +*/ +void mimetype_test_cmd(void){ + int i; + mimetype_verify(); + db_find_and_open_repository(0, 0); + for(i=2; i<g.argc; i++){ + fossil_print("%-20s -> %s\n", g.argv[i], mimetype_from_name(g.argv[i])); + } +} + +/* +** WEBPAGE: mimetype_list +** +** Show the built-in table used to guess embedded document mimetypes +** from file suffixes. +*/ +void mimetype_list_page(void){ + int i; + char *zCustomList = 0; /* value of the mimetypes setting */ + int nCustomEntries = 0; /* number of entries in the mimetypes + ** setting */ + mimetype_verify(); + style_header("Mimetype List"); + @ <p>The Fossil <a href="%R/help/www/doc">/doc</a> page uses filename + @ suffixes and the following tables to guess at the appropriate mimetype + @ for each document. Mimetypes may be customized and overridden using + @ <a href="%R/help/mimetypes">the mimetypes config setting</a>.</p> + zCustomList = db_get("mimetypes",0); + if( zCustomList!=0 ){ + Blob list, entry, key, val; + @ <h1>Repository-specific mimetypes</h1> + @ <p>The following extension-to-mimetype mappings are defined via + @ the <a href="%R/help/mimetypes">mimetypes setting</a>.</p> + @ <table class='sortable mimetypetable' border=1 cellpadding=0 \ + @ data-column-types='tt' data-init-sort='0'> + @ <thead> + @ <tr><th>Suffix<th>Mimetype + @ </thead> + @ <tbody> + blob_set(&list, zCustomList); + while( blob_line(&list, &entry)>0 ){ + const char *zKey; + if( blob_token(&entry, &key)==0 ) continue; + if( blob_token(&entry, &val)==0 ) continue; + zKey = blob_str(&key); + if( zKey[0]=='.' ) zKey++; + @ <tr><td>%h(zKey)<td>%h(blob_str(&val))</tr> + nCustomEntries++; + } + fossil_free(zCustomList); + if( nCustomEntries==0 ){ + /* This can happen if the option is set to an empty/space-only + ** value. */ + @ <tr><td colspan="2"><em>none</em></tr> + } + @ </tbody></table> + } + @ <h1>Default built-in mimetypes</h1> + if(nCustomEntries>0){ + @ <p>Entries starting with an exclamation mark <em><strong>!</strong></em> + @ are overwritten by repository-specific settings.</p> + } + @ <table class='sortable mimetypetable' border=1 cellpadding=0 \ + @ data-column-types='tt' data-init-sort='1'> + @ <thead> + @ <tr><th>Suffix<th>Mimetype + @ </thead> + @ <tbody> + for(i=0; i<count(aMime); i++){ + const char *zFlag = ""; + if(nCustomEntries>0 && + mimetype_from_name_custom(aMime[i].zSuffix)!=0){ + zFlag = "<em><strong>!</strong></em> "; + } + @ <tr><td>%s(zFlag)%h(aMime[i].zSuffix)<td>%h(aMime[i].zMimetype)</tr> + } + @ </tbody></table> + style_table_sorter(); + style_finish_page(); +} + +/* +** Check to see if the file in the pContent blob is "embedded HTML". Return +** true if it is, and fill pTitle with the document title. +** +** An "embedded HTML" file is HTML that lacks a header and a footer. The +** standard Fossil header is prepended and the standard Fossil footer is +** appended. Otherwise, the file is displayed without change. +** +** Embedded HTML must be contained in a <div class='fossil-doc'> element. +** If that <div> also contains a data-title attribute, then the +** value of that attribute is extracted into pTitle and becomes the title +** of the document. +*/ +int doc_is_embedded_html(Blob *pContent, Blob *pTitle){ + const char *zIn = blob_str(pContent); + const char *zAttr; + const char *zValue; + int nAttr, nValue; + int seenClass = 0; + int seenTitle = 0; + + while( fossil_isspace(zIn[0]) ) zIn++; + if( fossil_strnicmp(zIn,"<div",4)!=0 ) return 0; + zIn += 4; + while( zIn[0] ){ + if( fossil_isspace(zIn[0]) ) zIn++; + if( zIn[0]=='>' ) break; + zAttr = zIn; + while( fossil_isalnum(zIn[0]) || zIn[0]=='-' ) zIn++; + nAttr = (int)(zIn - zAttr); + while( fossil_isspace(zIn[0]) ) zIn++; + if( zIn[0]!='=' ) continue; + zIn++; + while( fossil_isspace(zIn[0]) ) zIn++; + if( zIn[0]=='"' || zIn[0]=='\'' ){ + char cDelim = zIn[0]; + zIn++; + zValue = zIn; + while( zIn[0] && zIn[0]!=cDelim ) zIn++; + if( zIn[0]==0 ) return 0; + nValue = (int)(zIn - zValue); + zIn++; + }else{ + zValue = zIn; + while( zIn[0]!=0 && zIn[0]!='>' && zIn[0]!='/' + && !fossil_isspace(zIn[0]) ) zIn++; + if( zIn[0]==0 ) return 0; + nValue = (int)(zIn - zValue); + } + if( nAttr==5 && fossil_strnicmp(zAttr,"class",5)==0 ){ + if( nValue!=10 || fossil_strnicmp(zValue,"fossil-doc",10)!=0 ) return 0; + seenClass = 1; + if( seenTitle ) return 1; + } + if( nAttr==10 && fossil_strnicmp(zAttr,"data-title",10)==0 ){ + /* The text argument to data-title="" will have had any characters that + ** are special to HTML encoded. We need to decode these before turning + ** the text into a title, as the title text will be reencoded later */ + char *zTitle = mprintf("%.*s", nValue, zValue); + int i; + for(i=0; fossil_isspace(zTitle[i]); i++){} + html_to_plaintext(zTitle+i, pTitle, 0); + fossil_free(zTitle); + seenTitle = 1; + if( seenClass ) return 1; + } + } + return seenClass; +} + +/* +** Look for a file named zName in the check-in with RID=vid. Load the content +** of that file into pContent and return the RID for the file. Or return 0 +** if the file is not found or could not be loaded. +*/ +int doc_load_content(int vid, const char *zName, Blob *pContent){ + int writable; + int rid; /* The RID of the file being loaded */ + if( db_is_protected(PROTECT_READONLY) + || !db_is_writeable("repository") + ){ + writable = 0; + }else{ + writable = 1; + } + if( writable ){ + db_end_transaction(0); + db_begin_write(); + } + if( !db_table_exists("repository", "vcache") || !writable ){ + db_multi_exec( + "CREATE %s TABLE IF NOT EXISTS vcache(\n" + " vid INTEGER, -- check-in ID\n" + " fname TEXT, -- filename\n" + " rid INTEGER, -- artifact ID\n" + " PRIMARY KEY(vid,fname)\n" + ") WITHOUT ROWID", writable ? "" : "TEMPORARY" + ); + } + if( !db_exists("SELECT 1 FROM vcache WHERE vid=%d", vid) ){ + db_multi_exec( + "DELETE FROM vcache;\n" + "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;\n" + "INSERT INTO vcache(vid,fname,rid)" + " SELECT checkinID, filename, blob.rid FROM foci, blob" + " WHERE blob.uuid=foci.uuid" + " AND foci.checkinID=%d;", + vid + ); + } + rid = db_int(0, "SELECT rid FROM vcache" + " WHERE vid=%d AND fname=%Q", vid, zName); + if( rid && content_get(rid, pContent)==0 ){ + rid = 0; + } + return rid; +} + +/* +** Check to verify that z[i] is contained within HTML markup. +** +** This works by looking backwards in the string for the most recent +** '<' or '>' character. If a '<' is found first, then we assume that +** z[i] is within markup. If a '>' is seen or neither character is seen, +** then z[i] is not within markup. +*/ +static int isWithinHtmlMarkup(const char *z, int i){ + while( i>=0 && z[i]!='>' && z[i]!='<' ){ i--; } + return z[i]=='<'; +} + +/* +** Check to see if z[i] is contained within an href='...' of markup. +*/ +static int isWithinHref(const char *z, int i){ + while( i>5 + && !fossil_isspace(z[i]) + && z[i]!='\'' && z[i]!='"' + && z[i]!='>' + ){ i--; } + if( i<=6 ) return 0; + if( z[i]!='\'' && z[i]!='\"' ) return 0; + if( strncmp(&z[i-5],"href=",5)!=0 ) return 0; + if( !fossil_isspace(z[i-6]) ) return 0; + return 1; +} + +/* +** Transfer content to the output. During the transfer, when text of +** the following form is seen: +** +** href="$ROOT/..." +** action="$ROOT/..." +** href=".../doc/$CURRENT/..." +** +** Convert $ROOT to the root URI of the repository, and $CURRENT to the +** version number of the /doc/ document currently being displayed (if any). +** Allow ' in place of " and any case for href or action. +** +** Efforts are made to limit this translation to cases where the text is +** fully contained with an HTML markup element. +*/ +void convert_href_and_output(Blob *pIn){ + int i, base; + int n = blob_size(pIn); + char *z = blob_buffer(pIn); + for(base=0, i=7; i<n; i++){ + if( z[i]=='$' + && strncmp(&z[i],"$ROOT/", 6)==0 + && (z[i-1]=='\'' || z[i-1]=='"') + && i-base>=9 + && ((fossil_strnicmp(&z[i-6],"href=",5)==0 && fossil_isspace(z[i-7])) || + (fossil_strnicmp(&z[i-8],"action=",7)==0 && fossil_isspace(z[i-9])) ) + && isWithinHtmlMarkup(z, i-6) + ){ + blob_append(cgi_output_blob(), &z[base], i-base); + blob_appendf(cgi_output_blob(), "%R"); + base = i+5; + }else + if( z[i]=='$' + && strncmp(&z[i-5],"/doc/$CURRENT/", 11)==0 + && isWithinHref(z,i-5) + && isWithinHtmlMarkup(z, i-5) + && strncmp(g.zPath, "doc/",4)==0 + ){ + int j; + for(j=7; g.zPath[j] && g.zPath[j]!='/'; j++){} + blob_append(cgi_output_blob(), &z[base], i-base); + blob_appendf(cgi_output_blob(), "%.*s", j-4, g.zPath+4); + base = i+8; + } + } + blob_append(cgi_output_blob(), &z[base], i-base); +} + +/* +** Render a document as the reply to the HTTP request. The body +** of the document is contained in pBody. The body might be binary. +** The mimetype is in zMimetype. +*/ +void document_render( + Blob *pBody, /* Document content */ + const char *zMime, /* MIME-type */ + const char *zDefaultTitle, /* Default title */ + const char *zFilename /* Name of the file being rendered */ +){ + Blob title; + int isPopup = P("popup")!=0; + blob_init(&title,0,0); + if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0 ){ + Blob tail = BLOB_INITIALIZER; + style_adunit_config(ADUNIT_RIGHT_OK); + if( wiki_find_title(pBody, &title, &tail) ){ + if( !isPopup ) style_header("%s", blob_str(&title)); + wiki_convert(&tail, 0, WIKI_BUTTONS); + }else{ + if( !isPopup ) style_header("%s", zDefaultTitle); + wiki_convert(pBody, 0, WIKI_BUTTONS); + } + if( !isPopup ){ + document_emit_js(); + style_finish_page(); + } + blob_reset(&tail); + }else if( fossil_strcmp(zMime, "text/x-markdown")==0 ){ + Blob tail = BLOB_INITIALIZER; + markdown_to_html(pBody, &title, &tail); + if( !isPopup ){ + if( blob_size(&title)>0 ){ + markdown_dehtmlize_blob(&title); + style_header("%s", blob_str(&title)); + }else{ + style_header("%s", zDefaultTitle); + } + } + convert_href_and_output(&tail); + if( !isPopup ){ + document_emit_js(); + style_finish_page(); + } + blob_reset(&tail); + }else if( fossil_strcmp(zMime, "text/x-org")==0 ){ + Blob tail = BLOB_INITIALIZER; + org_to_html(pBody, &title, &tail); + if( !isPopup ){ + style_header("%s", blob_size(&title) ? blob_str(&title) : zDefaultTitle); + } + convert_href_and_output(&tail); + if( !isPopup ) style_finish_page(); + blob_reset(&tail); + }else if( fossil_strcmp(zMime, "text/plain")==0 ){ + style_header("%s", zDefaultTitle); + @ <blockquote><pre> + @ %h(blob_str(pBody)) + @ </pre></blockquote> + document_emit_js(); + style_finish_page(); + }else if( fossil_strcmp(zMime, "text/html")==0 + && doc_is_embedded_html(pBody, &title) ){ + if( blob_size(&title)==0 ) blob_append(&title,zFilename,-1); + if( !isPopup ) style_header("%s", blob_str(&title)); + convert_href_and_output(pBody); + if( !isPopup ){ + document_emit_js(); + style_finish_page(); + } + }else if( fossil_strcmp(zMime, "text/x-pikchr")==0 ){ + style_adunit_config(ADUNIT_RIGHT_OK); + if( !isPopup ) style_header("%s", zDefaultTitle); + wiki_render_by_mimetype(pBody, zMime); + if( !isPopup ) style_finish_page(); +#ifdef FOSSIL_ENABLE_TH1_DOCS + }else if( Th_AreDocsEnabled() && + fossil_strcmp(zMime, "application/x-th1")==0 ){ + int raw = P("raw")!=0; + if( !raw ){ + Blob tail; + blob_zero(&tail); + if( wiki_find_title(pBody, &title, &tail) ){ + style_header("%s", blob_str(&title)); + Th_Render(blob_str(&tail)); + blob_reset(&tail); + }else{ + style_header("%h", zFilename); + Th_Render(blob_str(pBody)); + } + }else{ + Th_Render(blob_str(pBody)); + } + if( !raw ){ + document_emit_js(); + style_finish_page(); + } +#endif + }else{ + fossil_free(style_csp(1)); + cgi_set_content_type(zMime); + cgi_set_content(pBody); + } +} + + +/* +** WEBPAGE: uv +** WEBPAGE: doc +** +** URLs can be either /doc/CHECKIN/FILE or /uv/FILE. +** +** CHECKIN can be either tag or hash prefix or timestamp identifying a +** particular check-in, or the name of a branch (meaning the most recent +** check-in on that branch) or one of various magic words: +** +** "tip" means the most recent check-in +** +** "ckout" means the current check-out, if the server is run from +** within a check-out, otherwise it is the same as "tip" +** +** "latest" means use the most recent check-in for the document +** regardless of what branch it occurs on. +** +** FILE is the name of a file to delivered up as a webpage. FILE is relative +** to the root of the source tree of the repository. The FILE must +** be a part of CHECKIN, except when CHECKIN=="ckout" when FILE is read +** directly from disk and need not be a managed file. For /uv, FILE +** can also be the hash of the unversioned file. +** +** The "ckout" CHECKIN is intended for development - to provide a mechanism +** for looking at what a file will look like using the /doc webpage after +** it gets checked in. Some commands like "fossil ui", "fossil server", +** and "fossil http" accept an argument "--ckout-alias NAME" when allows +** NAME to be understood as an alias for "ckout". On a site with many +** embedded hyperlinks to /doc/trunk/... one can run with "--ckout-alias trunk" +** to simulate what the pending changes will look like after they are +** checked in. The NAME alias is stored in g.zCkoutAlias. +** +** The file extension is used to decide how to render the file. +** +** If FILE ends in "/" then the names "FILE/index.html", "FILE/index.wiki", +** and "FILE/index.md" are tried in that order. If the binary was compiled +** with TH1 embedded documentation support and the "th1-docs" setting is +** enabled, the name "FILE/index.th1" is also tried. If none of those are +** found, then FILE is completely replaced by "404.md" and tried. If that +** is not found, then a default 404 screen is generated. +** +** If the file's mimetype is "text/x-fossil-wiki" or "text/x-markdown" +** then headers and footers are added. If the document has mimetype +** text/html then headers and footers are usually not added. However, +** if a "text/html" document begins with the following div: +** +** <div class='fossil-doc' data-title='TEXT'> +** +** then headers and footers are supplied. The optional data-title field +** specifies the title of the document in that case. +** +** For fossil-doc documents and for markdown documents, text of the +** form: "href='$ROOT/" or "action='$ROOT" has the $ROOT name expanded +** to the top-level of the repository. +*/ +void doc_page(void){ + const char *zName = 0; /* Argument to the /doc page */ + const char *zOrigName = "?"; /* Original document name */ + const char *zMime; /* Document MIME type */ + char *zCheckin = "tip"; /* The check-in holding the document */ + char *zPathSuffix = ""; /* Text to append to g.zPath */ + int vid = 0; /* Artifact of check-in */ + int rid = 0; /* Artifact of file */ + int i; /* Loop counter */ + Blob filebody; /* Content of the documentation file */ + Blob title; /* Document title */ + int nMiss = (-1); /* Failed attempts to find the document */ + int isUV = g.zPath[0]=='u'; /* True for /uv. False for /doc */ + const char *zDfltTitle; + static const char *const azSuffix[] = { + "index.html", "index.wiki", "index.md", "index.org" +#ifdef FOSSIL_ENABLE_TH1_DOCS + , "index.th1" +#endif + }; + + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + style_set_current_feature("doc"); + blob_init(&title, 0, 0); + blob_init(&filebody, 0, 0); + zDfltTitle = isUV ? "" : "Documentation"; + db_begin_transaction(); + while( rid==0 && (++nMiss)<=count(azSuffix) ){ + zName = P("name"); + if( isUV ){ + if( zName==0 ) zName = "index.wiki"; + i = 0; + }else{ + if( zName==0 || zName[0]==0 ) zName = "tip/index.wiki"; + for(i=0; zName[i] && zName[i]!='/'; i++){} + zCheckin = mprintf("%.*s", i, zName); + if( fossil_strcmp(zCheckin,"ckout")==0 && g.localOpen==0 ){ + zCheckin = "tip"; + }else if( fossil_strcmp(zCheckin,"latest")==0 ){ + char *zNewCkin = db_text(0, + "SELECT uuid FROM blob, mlink, event, filename" + " WHERE filename.name=%Q" + " AND mlink.fnid=filename.fnid" + " AND blob.rid=mlink.mid" + " AND event.objid=mlink.mid" + " ORDER BY event.mtime DESC LIMIT 1", + zName + i + 1); + if( zNewCkin ) zCheckin = zNewCkin; + } + } + if( nMiss==count(azSuffix) ){ + zName = "404.md"; + zDfltTitle = "Not Found"; + }else if( zName[i]==0 ){ + assert( nMiss>=0 && nMiss<count(azSuffix) ); + zName = azSuffix[nMiss]; + }else if( !isUV ){ + zName += i; + } + while( zName[0]=='/' ){ zName++; } + if( isUV ){ + zPathSuffix = fossil_strdup(zName); + }else{ + zPathSuffix = mprintf("%s/%s", zCheckin, zName); + } + if( nMiss==0 ) zOrigName = zName; + if( !file_is_simple_pathname(zName, 1) ){ + if( sqlite3_strglob("*/", zName)==0 ){ + assert( nMiss>=0 && nMiss<count(azSuffix) ); + zName = mprintf("%s%s", zName, azSuffix[nMiss]); + if( !file_is_simple_pathname(zName, 1) ){ + goto doc_not_found; + } + }else{ + goto doc_not_found; + } + } + if( isUV ){ + if( db_table_exists("repository","unversioned") ){ + rid = unversioned_content(zName, &filebody); + if( rid==1 ){ + Stmt q; + db_prepare(&q, "SELECT hash, mtime FROM unversioned" + " WHERE name=%Q", zName); + if( db_step(&q)==SQLITE_ROW ){ + etag_check(ETAG_HASH, db_column_text(&q,0)); + etag_last_modified(db_column_int64(&q,1)); + } + db_finalize(&q); + }else if( rid==2 ){ + zName = db_text(zName, + "SELECT name FROM unversioned WHERE hash=%Q", zName); + g.isConst = 1; + } + zDfltTitle = zName; + } + }else if( fossil_strcmp(zCheckin,"ckout")==0 + || fossil_strcmp(zCheckin,g.zCkoutAlias)==0 + ){ + /* Read from the local check-out */ + char *zFullpath; + db_must_be_within_tree(); + zFullpath = mprintf("%s/%s", g.zLocalRoot, zName); + if( file_isfile(zFullpath, RepoFILE) + && blob_read_from_file(&filebody, zFullpath, RepoFILE)>0 ){ + rid = 1; /* Fake RID just to get the loop to end */ + } + fossil_free(zFullpath); + }else{ + vid = symbolic_name_to_rid(zCheckin, "ci"); + rid = vid>0 ? doc_load_content(vid, zName, &filebody) : 0; + } + } + g.zPath = mprintf("%s/%s", g.zPath, zPathSuffix); + if( rid==0 ) goto doc_not_found; + blob_to_utf8_no_bom(&filebody, 0); + + /* The file is now contained in the filebody blob. Deliver the + ** file to the user + */ + zMime = nMiss==0 ? P("mimetype") : 0; + if( zMime==0 ){ + zMime = mimetype_from_name(zName); + } + Th_StoreUnsafe("doc_name", zName); + if( vid ){ + Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'" + " FROM blob WHERE rid=%d", vid)); + Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event" + " WHERE objid=%d AND type='ci'", vid)); + } + cgi_check_for_malice(); + document_render(&filebody, zMime, zDfltTitle, zName); + if( nMiss>=count(azSuffix) ) cgi_set_status(404, "Not Found"); + db_end_transaction(0); + blob_reset(&title); + blob_reset(&filebody); + return; + + /* Jump here when unable to locate the document */ +doc_not_found: + db_end_transaction(0); + if( isUV && P("name")==0 ){ + uvlist_page(); + return; + } + cgi_set_status(404, "Not Found"); + style_header("Not Found"); + @ <p>Document %h(zOrigName) not found + if( fossil_strcmp(zCheckin,"ckout")!=0 ){ + @ in %z(href("%R/tree?ci=%T",zCheckin))%h(zCheckin)</a> + } + style_finish_page(); + blob_reset(&title); + blob_reset(&filebody); + return; +} + +/* +** The default logo. +*/ +static const unsigned char aLogo[] = { + 71, 73, 70, 56, 55, 97, 62, 0, 71, 0, 244, 0, 0, 85, + 129, 149, 95, 136, 155, 99, 139, 157, 106, 144, 162, 113, 150, 166, + 116, 152, 168, 127, 160, 175, 138, 168, 182, 148, 176, 188, 159, 184, + 195, 170, 192, 202, 180, 199, 208, 184, 202, 210, 191, 207, 215, 201, + 215, 221, 212, 223, 228, 223, 231, 235, 226, 227, 226, 226, 234, 237, + 233, 239, 241, 240, 244, 246, 244, 247, 248, 255, 255, 255, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 0, 0, + 0, 0, 62, 0, 71, 0, 0, 5, 255, 96, 100, 141, 100, 105, + 158, 168, 37, 41, 132, 192, 164, 112, 44, 207, 102, 99, 0, 56, + 16, 84, 116, 239, 199, 141, 65, 110, 232, 248, 25, 141, 193, 161, + 82, 113, 108, 202, 32, 55, 229, 210, 73, 61, 41, 164, 88, 102, + 181, 10, 41, 96, 179, 91, 106, 35, 240, 5, 135, 143, 137, 242, + 87, 123, 246, 33, 190, 81, 108, 163, 237, 198, 14, 30, 113, 233, + 131, 78, 115, 72, 11, 115, 87, 101, 19, 124, 51, 66, 74, 8, + 19, 16, 67, 100, 74, 133, 50, 15, 101, 135, 56, 11, 74, 6, + 143, 49, 126, 106, 56, 8, 145, 67, 9, 152, 48, 139, 155, 5, + 22, 13, 74, 115, 161, 41, 147, 101, 13, 130, 57, 132, 170, 40, + 167, 155, 0, 94, 57, 3, 178, 48, 183, 181, 57, 160, 186, 40, + 19, 141, 189, 0, 69, 192, 40, 16, 195, 155, 185, 199, 41, 201, + 189, 191, 205, 193, 188, 131, 210, 49, 175, 88, 209, 214, 38, 19, + 3, 11, 19, 111, 127, 60, 219, 39, 55, 204, 19, 11, 6, 100, + 5, 10, 227, 228, 37, 163, 0, 239, 117, 56, 238, 243, 49, 195, + 177, 247, 48, 158, 56, 251, 50, 216, 254, 197, 56, 128, 107, 158, + 2, 125, 171, 114, 92, 218, 246, 96, 66, 3, 4, 50, 134, 176, + 145, 6, 97, 64, 144, 24, 19, 136, 108, 91, 177, 160, 0, 194, + 19, 253, 0, 216, 107, 214, 224, 192, 129, 5, 16, 83, 255, 244, + 43, 213, 195, 24, 159, 27, 169, 64, 230, 88, 208, 227, 129, 182, + 54, 4, 89, 158, 24, 181, 163, 199, 1, 155, 52, 233, 8, 130, + 176, 83, 24, 128, 137, 50, 18, 32, 48, 48, 114, 11, 173, 137, + 19, 110, 4, 64, 105, 1, 194, 30, 140, 68, 15, 24, 24, 224, + 50, 76, 70, 0, 11, 171, 54, 26, 160, 181, 194, 149, 148, 40, + 174, 148, 122, 64, 180, 208, 161, 17, 207, 112, 164, 1, 128, 96, + 148, 78, 18, 21, 194, 33, 229, 51, 247, 65, 133, 97, 5, 250, + 69, 229, 100, 34, 220, 128, 166, 116, 190, 62, 8, 167, 195, 170, + 47, 163, 0, 130, 90, 152, 11, 160, 173, 170, 27, 154, 26, 91, + 232, 151, 171, 18, 14, 162, 253, 98, 170, 18, 70, 171, 64, 219, + 10, 67, 136, 134, 187, 116, 75, 180, 46, 179, 174, 135, 4, 189, + 229, 231, 78, 40, 10, 62, 226, 164, 172, 64, 240, 167, 170, 10, + 18, 124, 188, 10, 107, 65, 193, 94, 11, 93, 171, 28, 248, 17, + 239, 46, 140, 78, 97, 34, 25, 153, 36, 99, 65, 130, 7, 203, + 183, 168, 51, 34, 136, 25, 140, 10, 6, 16, 28, 255, 145, 241, + 230, 140, 10, 66, 178, 167, 112, 48, 192, 128, 129, 9, 31, 141, + 84, 138, 63, 163, 162, 2, 203, 206, 240, 56, 55, 98, 192, 188, + 15, 185, 50, 160, 6, 0, 125, 62, 33, 214, 195, 33, 5, 24, + 184, 25, 231, 14, 201, 245, 144, 23, 126, 104, 228, 0, 145, 2, + 13, 140, 244, 212, 17, 21, 20, 176, 159, 17, 95, 225, 160, 128, + 16, 1, 32, 224, 142, 32, 227, 125, 87, 64, 0, 16, 54, 129, + 205, 2, 141, 76, 53, 130, 103, 37, 166, 64, 144, 107, 78, 196, + 5, 192, 0, 54, 50, 229, 9, 141, 49, 84, 194, 35, 12, 196, + 153, 48, 192, 137, 57, 84, 24, 7, 87, 159, 249, 240, 215, 143, + 105, 241, 118, 149, 9, 139, 4, 64, 203, 141, 35, 140, 129, 131, + 16, 222, 125, 231, 128, 2, 238, 17, 152, 66, 3, 5, 56, 224, + 159, 103, 16, 76, 25, 75, 5, 11, 164, 215, 96, 9, 14, 16, + 36, 225, 15, 11, 40, 144, 192, 156, 41, 10, 178, 199, 3, 66, + 64, 80, 193, 3, 124, 90, 48, 129, 129, 102, 177, 18, 192, 154, + 49, 84, 240, 208, 92, 22, 149, 96, 39, 9, 31, 74, 17, 94, + 3, 8, 177, 199, 72, 59, 85, 76, 25, 216, 8, 139, 194, 197, + 138, 163, 69, 96, 115, 0, 147, 72, 72, 84, 28, 14, 79, 86, + 233, 230, 23, 113, 26, 160, 128, 3, 10, 58, 129, 103, 14, 159, + 214, 163, 146, 117, 238, 213, 154, 128, 151, 109, 84, 64, 217, 13, + 27, 10, 228, 39, 2, 235, 164, 168, 74, 8, 0, 59, +}; + +/* +** WEBPAGE: logo +** +** Return the logo image. This image is available to anybody who can see +** the login page. It is designed for use in the upper left-hand corner +** of the header. +*/ +void logo_page(void){ + Blob logo; + char *zMime; + + etag_check(ETAG_CONFIG, 0); + zMime = db_get("logo-mimetype", "image/gif"); + blob_zero(&logo); + db_blob(&logo, "SELECT value FROM config WHERE name='logo-image'"); + if( blob_size(&logo)==0 ){ + blob_init(&logo, (char*)aLogo, sizeof(aLogo)); + } + cgi_set_content_type(zMime); + cgi_set_content(&logo); +} + +/* +** The default background image: a 16x16 white GIF +*/ +static const unsigned char aBackground[] = { + 71, 73, 70, 56, 57, 97, 16, 0, 16, 0, + 240, 0, 0, 255, 255, 255, 0, 0, 0, 33, + 254, 4, 119, 105, 115, 104, 0, 44, 0, 0, + 0, 0, 16, 0, 16, 0, 0, 2, 14, 132, + 143, 169, 203, 237, 15, 163, 156, 180, 218, 139, + 179, 62, 5, 0, 59, +}; + + +/* +** WEBPAGE: background +** +** Return the background image. If no background image is defined, a +** built-in 16x16 pixel white GIF is returned. +*/ +void background_page(void){ + Blob bgimg; + char *zMime; + + etag_check(ETAG_CONFIG, 0); + zMime = db_get("background-mimetype", "image/gif"); + blob_zero(&bgimg); + db_blob(&bgimg, "SELECT value FROM config WHERE name='background-image'"); + if( blob_size(&bgimg)==0 ){ + blob_init(&bgimg, (char*)aBackground, sizeof(aBackground)); + } + cgi_set_content_type(zMime); + cgi_set_content(&bgimg); +} + + +/* +** WEBPAGE: favicon.ico +** +** Return the configured "favicon.ico" image. If no "favicon.ico" image +** is defined, the returned image is for the Fossil lizard icon. +** +** The intended use case here is to supply an icon for the "fossil ui" +** command. For a permanent website, the recommended process is for +** the admin to set up a project-specific icon and reference that icon +** in the HTML header using a line like: +** +** <link rel="icon" href="URL-FOR-YOUR-ICON" type="MIMETYPE"/> +** +*/ +void favicon_page(void){ + Blob icon; + char *zMime; + + etag_check(ETAG_CONFIG, 0); + zMime = db_get("icon-mimetype", "image/gif"); + blob_zero(&icon); + db_blob(&icon, "SELECT value FROM config WHERE name='icon-image'"); + if( blob_size(&icon)==0 ){ + blob_init(&icon, (char*)aLogo, sizeof(aLogo)); + } + cgi_set_content_type(zMime); + cgi_set_content(&icon); +} + +/* +** WEBPAGE: docsrch +** +** Search for documents that match a user-supplied full-text search pattern. +** If no pattern is specified (by the s= query parameter) then the user +** is prompted to enter a search string. +** +** Query parameters: +** +** s=PATTERN Search for PATTERN +*/ +void doc_search_page(void){ + const int isSearch = P("s")!=0; + login_check_credentials(); + style_header("Document Search%s", isSearch ? " Results" : ""); + cgi_check_for_malice(); + search_screen(SRCH_DOC, 0); + style_finish_page(); +} diff --git a/src/sounds/d.wav b/src/sounds/d.wav new file mode 100644 index 0000000..fd18062 Binary files /dev/null and b/src/sounds/d.wav differ diff --git a/test/delta1.test b/test/delta1.test new file mode 100644 index 0000000..82d7936 --- /dev/null +++ b/test/delta1.test @@ -0,0 +1,60 @@ +# +# Copyright (c) 2006 D. Richard Hipp +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the Simplified BSD License (also +# known as the "2-Clause License" or "FreeBSD License".) +# +# This program is distributed in the hope that it will be useful, +# but without any warranty; without even the implied warranty of +# merchantability or fitness for a particular purpose. +# +# Author contact information: +# drh@hwaci.com +# http://www.hwaci.com/drh/ +# +############################################################################ +# +# Tests of the delta mechanism. +# + +test_setup "" + +# Use test script files as the basis for this test. +# +# For each test, copy the file intact to "./t1". Make +# some random changes in "./t2". Then call test-delta on the +# two files to make sure that deltas between these two files +# work properly. +# +set filelist [lsort [glob $testdir/*]] +foreach f $filelist { + if {[file isdir $f]} continue + set base [file root [file tail $f]] + set f1 [read_file $f] + write_file t1 $f1 + for {set i 0} {$i<100} {incr i} { + write_file t2 [random_changes $f1 1 1 0 0.1] + fossil test-delta t1 t2 + test delta-$base-$i-1 {[normalize_result]=="ok"} + write_file t2 [random_changes $f1 1 1 0 0.2] + fossil test-delta t1 t2 + test delta-$base-$i-2 {[normalize_result]=="ok"} + write_file t2 [random_changes $f1 1 1 0 0.4] + fossil test-delta t1 t2 + test delta-$base-$i-3 {[normalize_result]=="ok"} + } +} + +set empties { "" "" "" a a "" } +set i 0 +foreach {f1 f2} $empties { + incr i + write_file t1 $f1 + write_file t2 $f2 + fossil test-delta t1 t2 + test delta-empty-$i {[normalize_result]=="ok"} +} +############################################################################### + +test_cleanup diff --git a/test/diff-test-1.wiki b/test/diff-test-1.wiki new file mode 100644 index 0000000..46ba4ba --- /dev/null +++ b/test/diff-test-1.wiki @@ -0,0 +1,60 @@ +<title>Graph Test One + +This page contains list of URLs of interesting diffs. +Click on all URLs, one by one, to verify +the correct operation of the diff logic. +For correct testing, many of these require show-version-diffs +to be enabled in the [/setup_timeline|Timeline Display Preferences] +configuration page. + + * + Multiple edits on a single line. This is an SQLite version + update diff. It is a large diff and contains many other interesting + features. Scan the whole diff. + * Tricky alignment and multiple edits per line. + * Add a column to a table + * Column alignment with multibyte characters. + The edit of a line with multibyte characters is the first chunk. + * Large diff of sqlite3.c. This diff was very + slow prior to the performance enhancement change [9e15437e97]. + * + A difficult indentation change. UPDATE: Notice also the improved + multi-segment update marks on lines 122648 and 122763 on the new side. + * Inverse of the previous. + * Another tricky indentation. Notice especially + lines 59398 and 59407 on the left. + * Inverse of the previous. + * A complex change that is difficult to align, and + hence falls back to the "delete left and insert right" strategy. + * Inverse of the previous. + * sqlite3.c changes + that are difficult to align. + * sqlite3.c changes inverted. + * Lorem Ipsum in Greek. + * Lorem Ipsum in Greek inverted. + +External: + + * + Code indentation change. + * + A complex change (chunk 1) in which the alignment becomes so complex + that it is better for clarity to abandon it and just show the left + and right sides contiguously. + * + An indentation change. See especially lines 2313 and 2317 on the right, + that their green indentation addition is left-justified. diff --git a/test/diff.test b/test/diff.test new file mode 100644 index 0000000..4b02329 --- /dev/null +++ b/test/diff.test @@ -0,0 +1,162 @@ +# +# Copyright (c) 2016 D. Richard Hipp +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the Simplified BSD License (also +# known as the "2-Clause License" or "FreeBSD License".) +# +# This program is distributed in the hope that it will be useful, +# but without any warranty; without even the implied warranty of +# merchantability or fitness for a particular purpose. +# +# Author contact information: +# drh@hwaci.com +# http://www.hwaci.com/drh/ +# +############################################################################ +# +# Tests for the diff command. +# + +require_no_open_checkout + +test_setup; set rootDir [file normalize [pwd]] + +################################### +# Tests of binary file detection. # +################################### + +file mkdir .fossil-settings +write_file [file join .fossil-settings binary-glob] "*" + +write_file file0.dat ""; # no content. +write_file file1.dat "test file 1 (one line no term)." +write_file file2.dat "test file 2 (NUL character).\0" +write_file file3.dat "test file 3 (long line).[string repeat x 32768]" +write_file file4.dat "test file 4 (long line).[string repeat y 32768]\ntwo" +write_file file5.dat "[string repeat z 32768]\ntest file 5 (long line)." + +fossil add $rootDir +fossil commit -m "c1" + +############################################################################### + +fossil ls +test diff-ls-1 {[normalize_result] eq \ +"file0.dat\nfile1.dat\nfile2.dat\nfile3.dat\nfile4.dat\nfile5.dat"} + +############################################################################### + +write_file file0.dat "\0" +fossil diff file0.dat + +test diff-file0-1 {[normalize_result] eq {Index: file0.dat +================================================================== +--- file0.dat ++++ file0.dat +cannot compute difference between binary files}} + +############################################################################### + +write_file file1.dat [string repeat z 32768] +fossil diff file1.dat + +test diff-file1-1 {[normalize_result] eq {Index: file1.dat +================================================================== +--- file1.dat ++++ file1.dat +cannot compute difference between binary files}} + +############################################################################### + +write_file file2.dat "test file 2 (no NUL character)." +fossil diff file2.dat + +test diff-file2-1 {[normalize_result] eq {Index: file2.dat +================================================================== +--- file2.dat ++++ file2.dat +cannot compute difference between binary files}} + +############################################################################### + +write_file file3.dat "test file 3 (not a long line)." +fossil diff file3.dat + +test diff-file3-1 {[normalize_result] eq {Index: file3.dat +================================================================== +--- file3.dat ++++ file3.dat +cannot compute difference between binary files}} + +############################################################################### + +write_file file4.dat "test file 4 (not a long line).\ntwo" +fossil diff file4.dat + +test diff-file4-1 {[normalize_result] eq {Index: file4.dat +================================================================== +--- file4.dat ++++ file4.dat +cannot compute difference between binary files}} + +############################################################################### + +write_file file5.dat "[string repeat 0 16]\ntest file 5 (not a long line)." +fossil diff file5.dat + +test diff-file5-1 {[normalize_result] eq {Index: file5.dat +================================================================== +--- file5.dat ++++ file5.dat +cannot compute difference between binary files}} + +############################################################################### + +write_file file6a.dat "{\n \"abc\": {\n \"def\": false,\n \"ghi\": false\n }\n}\n" +write_file file6b.dat "{\n \"abc\": {\n \"def\": false,\n \"ghi\": false\n },\n \"jkl\": {\n \"mno\": {\n \"pqr\": false\n }\n }\n}\n" +fossil xdiff -y -W 16 file6a.dat file6b.dat +test diff-file-6-1 {[normalize_result] eq {========== file6a.dat ===== versus ===== file6b.dat ===== + 1 { 1 { + 2 "abc": { 2 "abc": { + 3 "def": false, 3 "def": false, + 4 "ghi": false 4 "ghi": false + > 5 }, + > 6 "jkl": { + > 7 "mno": { + > 8 "pqr": false + > 9 } + 5 } 10 } + 6 } 11 }}} + +############################################################################### + +fossil rm file1.dat +fossil diff -v file1.dat + +test diff-deleted-file-1 {[normalize_result] eq {DELETED file1.dat +Index: file1.dat +================================================================== +--- file1.dat ++++ /dev/null +@@ -1,1 +0,0 @@ +-test file 1 (one line no term).}} + +############################################################################### + +write_file file6.dat "test file 6 (one line no term)." +fossil add file6.dat + +fossil diff -v file6.dat + +test diff-added-file-1 {[normalize_result] eq {ADDED file6.dat +Index: file6.dat +================================================================== +--- /dev/null ++++ file6.dat +@@ -0,0 +1,1 @@ ++test file 6 (one line no term).}} + +############################################################################### + +test_cleanup diff --git a/tools/copland/dist/copland-skin.config b/tools/copland/dist/copland-skin.config new file mode 100644 index 0000000..02392e8 --- /dev/null +++ b/tools/copland/dist/copland-skin.config @@ -0,0 +1,827 @@ +# Copland Fossil skin configuration +# Generated deterministically by tools/copland/scripts/build-config.mjs. +# Import with: fossil configuration import dist/copland-skin.config -R REPOSITORY +config /config 12869 +1 'css' value ':root { + color-scheme: light dark; + --page-bg: #fff; + --surface: #fff; + --surface-subtle: #f5f5f5; + --surface-muted: #e9ecef; + --surface-active: #ddd; + --text: #212529; + --text-muted: #6c757d; + --text-faint: #767676; + --border: #ced4da; + --border-strong: #888; + --link: #0645d9; + --link-hover: #002f9f; + --primary: #0056d6; + --primary-hover: #0044aa; + --primary-text: #fff; + --danger: #b00020; + --danger-bg: #f8d7da; + --success: #146c2e; + --success-bg: #d4edda; + --warning-bg: #fff3cd; + --info-bg: #d1ecf1; + --code-bg: #f1f3f5; + --selection: #b9d6ff; + --focus: #ffbf47; + --shadow: rgba(0, 0, 0, 0.12); + --container: 71.25rem; + --gutter: 1rem; + --radius: 0; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --page-bg: #212529; + --surface: #212529; + --surface-subtle: #191c1f; + --surface-muted: #2d3237; + --surface-active: #3b4147; + --text: #f1f3f5; + --text-muted: #c2c7cc; + --text-faint: #adb5bd; + --border: #565e65; + --border-strong: #8b949c; + --link: #5c9cff; + --link-hover: #91bcff; + --primary: #4d8ee8; + --primary-hover: #70a7f2; + --primary-text: #101214; + --danger: #ff6b78; + --danger-bg: #57242a; + --success: #65d47d; + --success-bg: #183d24; + --warning-bg: #514318; + --info-bg: #193e47; + --code-bg: #17191b; + --selection: #173f70; + --focus: #ffd166; + --shadow: rgba(0, 0, 0, 0.4); + } +} +* { + box-sizing: border-box; +} + +html { + min-height: 100%; + scrollbar-gutter: stable; + font-size: 16px; +} + +body { + min-height: 100vh; + margin: 0; + padding: 0 0 1rem; + background: var(--page-bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 1rem; + line-height: 1.5; + text-align: initial; +} + +::selection { + background: var(--selection); +} + +.container, +div.content { + width: 100%; + max-width: var(--container); + margin-inline: auto; + padding-inline: var(--gutter); +} + +div.content { + min-height: 45vh; +} + +body a { + color: var(--link); + text-decoration: underline; + text-underline-offset: 0.12em; +} + +body a:hover { + color: var(--link-hover); + text-decoration: none; +} + +a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, +textarea:focus-visible, [tabindex]:focus-visible { + outline: 3px solid var(--focus); + outline-offset: 2px; +} + +h1, h2, h3, h4, h5, h6 { + margin: 1rem 0 0.5rem; + color: var(--text); + font-weight: 500; + line-height: 1.2; +} + +h1 { + font-size: 1.75rem; +} + +h2 { + font-size: 1.45rem; +} + +h3 { + font-size: 1.15rem; + border-bottom: 1px solid var(--border); +} + +h4 { + font-size: 1rem; + border-bottom: 1px solid var(--border); +} + +p { + margin: 0 0 1rem; +} + +hr { + border: 0; + border-top: 1px solid var(--border); +} + +.site-header { + margin-bottom: 0; +} + +.site-header-row { + min-height: 3.25rem; + display: flex; + align-items: center; + gap: 1rem; +} + +.site-brand, +.resource-title a, +.account-nav a, +.mainmenu a { + text-decoration: none; +} + +.site-brand { + flex: 0 0 auto; + color: var(--text); + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.025em; +} + +.site-brand::before { + content: ""; + display: inline-block; + width: 0.78rem; + height: 0.78rem; + margin-right: 0.42rem; + border: 2px solid currentColor; + border-radius: 50%; + vertical-align: 0.02rem; +} + +.site-brand:hover, +.resource-title a:hover, +.account-nav a:hover, +.mainmenu a:hover { + color: var(--text); + text-decoration: underline; +} + +.resource-title { + min-width: 0; + display: flex; + align-items: baseline; + gap: 0.4rem; + overflow: hidden; + white-space: nowrap; +} + +.resource-title a, +.resource-page { + overflow: hidden; + text-overflow: ellipsis; +} + +.resource-title a { + color: var(--text); + font-weight: 600; +} + +.resource-separator, .resource-page { + color: var(--text-muted); +} + +.account-nav { + margin-left: auto; + white-space: nowrap; + font-size: 0.9rem; +} + +.resource-nav { + border-bottom: 3px solid var(--surface-active); +} + +.mainmenu { + min-height: 2rem; + display: flex; + align-items: end; + gap: 0; + overflow-x: auto; + scrollbar-width: thin; +} + +.mainmenu a { + flex: 0 0 auto; + padding: 0.25rem 0.85rem 0.18rem; + color: var(--text-muted); + white-space: nowrap; +} + +.mainmenu a.active, +.mainmenu a.selected, +.mainmenu a:hover { + color: var(--text); +} + +.mainmenu a.active, +.mainmenu a.selected { + background: var(--surface-active); +} + +.page-title { + margin-top: 0.9rem; + margin-bottom: 0.75rem; + font-size: 1.35rem; + font-weight: 500; +} + +.site-footer { + margin-top: 2rem; + color: var(--text-muted); + font-size: 0.8rem; +} + +.site-footer .container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.4rem; +} + +pre, code, kbd, samp, tt { + font-family: var(--font-mono); +} + +code, tt { + overflow-wrap: anywhere; +} + +pre { + max-width: 100%; + overflow: auto; + margin: 0 0 1rem; + padding: 0.75rem 1rem; + background: var(--code-bg); + color: var(--text); + font-size: 0.875rem; + line-height: 1.35; + tab-size: 4; +} + +pre a { + color: inherit; +} + +blockquote { + margin: 0 0 1rem; + padding-left: 1rem; + border-left: 4px solid var(--border); +} + +.org-document { + max-width: 100%; +} + +.org-document .org-title { + margin-top: 0; +} + +.org-metadata { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + gap: 0.15rem 0.75rem; + margin: 0 0 1rem; + color: var(--text-muted); + font-size: 0.92rem; +} + +.org-metadata dt { + font-weight: 600; +} + +.org-metadata dd { + margin: 0; +} + +.org-toc { + margin: 0 0 1.25rem; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + background: var(--surface-subtle); +} + +.org-toc h2 { + margin-top: 0; + font-size: 1.05rem; +} + +.org-toc ol { + margin: 0; + padding-left: 1.5rem; +} + +.org-toc-level-2 { + margin-left: 1rem; +} + +.org-toc-level-3 { + margin-left: 2rem; +} + +.org-toc-level-4, .org-toc-level-5, .org-toc-level-6 { + margin-left: 3rem; +} + +.org-table { + display: block; + overflow-x: auto; +} + +.org-block { + border-left: 3px solid var(--border-strong); +} + +.org-example { + color: var(--text); +} + +.org-unknown-block { + border-left-color: var(--danger); +} + +table { + width: 100%; + margin-bottom: 1rem; + border-collapse: collapse; + color: var(--text); +} + +th { + text-align: left; + font-weight: 600; +} + +th, td { + padding: 0.28rem 0.5rem; + vertical-align: top; + border-top: 1px solid var(--border); +} + +thead th { + border-bottom: 2px solid var(--border-strong); +} + +tbody tr:nth-child(odd) { + background: color-mix(in srgb, var(--surface-subtle) 70%, transparent); +} + +table.browser, table.report, table.timelineTable { + font-size: 0.92rem; +} + +table.browser td:first-child, table.browser td:nth-child(2) { + font-family: var(--font-mono); +} + +input, button, select, textarea { + font: inherit; +} + +input[type=text], input[type=search], input[type=email], input[type=password], +input[type=url], input[type=number], textarea, select { + max-width: 100%; + padding: 0.35rem 0.5rem; + border: 1px solid var(--border-strong); + border-radius: var(--radius); + background: var(--surface); + color: var(--text); +} + +textarea { + width: 100%; +} + +label { + display: inline-block; + margin-bottom: 0.25rem; +} + +button, input[type=submit], input[type=button], .button, a.button { + display: inline-block; + padding: 0.35rem 0.7rem; + border: 1px solid var(--primary); + border-radius: var(--radius); + background: var(--primary); + color: var(--primary-text); + cursor: pointer; + line-height: 1.25; + text-decoration: none; +} + +button:hover, input[type=submit]:hover, input[type=button]:hover, .button:hover { + border-color: var(--primary-hover); + background: var(--primary-hover); +} + +button:disabled, input:disabled { + cursor: not-allowed; + opacity: 0.65; +} + +fieldset { + margin: 0 0 1rem; + border: 1px solid var(--border); +} + +legend { + padding-inline: 0.35rem; +} + +.submenu, .sectionmenu { + margin: 0 0 1rem; + padding: 0.4rem 0.6rem; + background: var(--surface-subtle); + border: 1px solid var(--border); +} + +.submenu a, .sectionmenu a { + margin-right: 0.75rem; +} + +.generalError, .error, .fatalError, .loginError, +.warning, .notice, .success { + margin: 0.75rem 0; + padding: 0.65rem 0.8rem; + border: 1px solid var(--border); +} + +.generalError, .error, .fatalError, .loginError { + background: var(--danger-bg); + color: var(--text); +} + +.warning { + background: var(--warning-bg); +} + +.notice { + background: var(--info-bg); +} + +.success { + background: var(--success-bg); +} + +.timelineDate { + margin-top: 1rem; + color: var(--text); + font-weight: 600; +} + +.timelineHistDsp { + color: var(--text-muted); +} + +.timelineModernCell, .timelineColumnarCell, .timelineDetailCell { + padding-block: 0.35rem; +} + +.timelineModernDetail { + opacity: 1; +} + +.timelineCompactComment, .timelineComment { + line-height: 1.35; +} + +.timelineTime { + color: var(--text-muted); + white-space: nowrap; +} + +.timelineGraph { + min-width: 2rem; +} + +.tl-node { + border-radius: 50%; +} + +.timelineModernCell a, +.timelineColumnarCell a, +.timelineDetailCell a { + color: var(--link) !important; +} + +.timelineModernCell a:hover, +.timelineColumnarCell a:hover, +.timelineDetailCell a:hover { + color: var(--link-hover) !important; +} + +.filetree { + font-family: var(--font-mono); + font-size: 0.9rem; +} + +.filetree ul { + padding-left: 1.25rem; +} + +.filetree a { + color: var(--text); +} + +.filetree a:hover { + color: var(--link); +} + +.fileage tr:hover, table.browser tr:hover { + background: var(--surface-active); +} + +.diff, .udiff, .sbsdiff { + font-family: var(--font-mono); + font-size: 0.82rem; +} + +.diff pre, .udiff pre, .sbsdiff pre { + background: transparent; +} + +.diffln, .diffhr { + color: var(--text-muted); + background: var(--surface-muted); +} + +.diffadd, .diffins { + background: var(--success-bg); +} + +.diffrm, .diffdel { + background: var(--danger-bg); +} + +.diffchng { + background: var(--warning-bg); +} + +.selectedText { + background: var(--selection); +} + +.forumPost, .forumTime, .forumSel, .forumHierRoot, .forumHier { + border-color: var(--border); +} + +.forumPost, .forumTime { + background: var(--surface-subtle); + color: var(--text); +} + +.forumPostBody { + overflow-wrap: anywhere; +} + +.forumPostBody img, .wiki-content img, .markdown img { + max-width: 100%; + height: auto; +} + +.ticketDspLabel, .ticketDspValue { + padding: 0.35rem 0.5rem; +} + +.ticketDspLabel { + color: var(--text-muted); + font-weight: 600; +} + +.ticketDspValue { + border-bottom: 1px solid var(--border); +} + +.branchname, .tagname, .wikiTagCancelled, .infoTagCancelled { + display: inline-block; + padding: 0.05rem 0.3rem; + background: var(--surface-muted); + color: var(--text); + font-size: 0.82rem; +} + +.statistics-report-table-events th, .statistics-report-table-events td { + text-align: right; +} + +.statistics-report-table-events th:first-child, .statistics-report-table-events td:first-child { + text-align: left; +} + +.accordion_panel { + border-color: var(--border); + background: var(--surface); +} + +.accordion_marker { + color: var(--text-muted); +} + +body.repolist .resource-title, +body.repolist .resource-nav, +body.repolist > .page-title { + display: none; +} + +body.repolist div.content { + padding-top: 1rem; +} + +body.repolist table td:first-child { + font-family: var(--font-mono); + font-weight: 600; +} + +@media (prefers-color-scheme: dark) { + img { + opacity: 0.92; + } + body .timelineModernCell, + body .timelineColumnarCell, + body .timelineDetailCell, + .timelineModernCell[style*=background-color], + .timelineColumnarCell[style*=background-color], + .timelineDetailCell[style*=background-color], + td[style*=background-color] { + background-color: var(--surface-subtle) !important; + color: var(--text) !important; + } + .diffadd, .diffins { + color: var(--text); + } + .diffrm, .diffdel { + color: var(--text); + } +} +@media (max-width: 48rem) { + :root { + --gutter: 0.75rem; + } + .site-header-row { + min-height: 2.8rem; + gap: 0.65rem; + } + .resource-page, .resource-separator { + display: none; + } + .mainmenu a { + padding-inline: 0.65rem; + } + .page-title { + font-size: 1.2rem; + } + table:not(.sbsdiffcols) { + display: block; + max-width: 100%; + overflow-x: auto; + } + .desktoponly { + display: none !important; + } +} +@media (max-width: 36rem) { + .site-brand { + font-size: 1.1rem; + } + .resource-title { + font-size: 0.9rem; + } + .account-nav { + font-size: 0.82rem; + } + .page-title { + margin-top: 0.7rem; + } + pre { + padding: 0.6rem 0.7rem; + } +} +@media print { + .resource-nav, .account-nav, .submenu, .sectionmenu, .site-footer { + display: none !important; + } + body, pre, code { + background: #fff !important; + color: #000 !important; + } + a { + color: #000; + text-decoration: underline; + } + div.content, .container { + max-width: none; + padding: 0; + } +} +' +config /config 1631 +1 'header' value '